Files
server/usr/share/psa-pear/pear/ingo-postfix-policyd
2026-01-07 20:52:11 +01:00

181 lines
5.6 KiB
PHP
Executable File

#!/usr/bin/env php
<?php
/**
* Usage: ingo-postfix-policyd [-v]
*
* Delegated Postfix SMTPD policy server that enforce's Ingo's
* blacklist and whitelist rules. Logging is done through the
* standard Horde logger.
*
* How it works: each time a Postfix SMTP server process is started it
* connects to the policy service socket, and Postfix runs one
* instance of this PHP script. By default, a Postfix SMTP server
* process terminates after 100 seconds of idle time, or after serving
* 100 clients. Thus, the cost of starting this script is smoothed out
* over time.
*
* To run this from /etc/postfix/master.cf (if necessary substituting
* a user that has permissions for your Horde configuration and
* logfiles for www-data):
*
* policy unix - n n - - spawn
* user=www-data argv=/path/to/horde/ingo/scripts/ingo-postfix-policyd
*
* To use this from Postfix SMTPD, use in /etc/postfix/main.cf:
*
* smtpd_recipient_restrictions =
* ...
* reject_unauth_destination
* check_policy_service unix:private/policy
* ...
*
* NOTE: specify check_policy_service AFTER reject_unauth_destination
* or else your system can become an open relay.
*
* To test this script by hand, execute:
*
* % ingo-postfix-policyd
*
* Each query is a bunch of attributes. See
* http://www.postfix.org/SMTPD_POLICY_README.html and the example
* greylisting policy daemon for all of the possibilities. This script
* uses only:
*
* sender=foo@bar.tld
* recipient=bar@foo.tld
*
* And the query is terminated with an:
* [empty line]
*
* The policy server script will answer in the same style, with an
* attribute list followed by a empty line:
*
* action=DUNNO
* [empty line]
*
* The possible actions are documented at
* http://www.postfix.org/access.5.html. We return "DUNNO" when the
* sender/recipient combination doesn't match any blacklist or
* whitelist rules, OK if the sender is whitelisted, and REJECT if the
* sender is blacklisted.
*
* Copyright 2012-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.
*
* @category Horde
* @license http://www.horde.org/licenses/apache ASL
* @package Ingo
*/
$baseFile = __DIR__ . '/../lib/Application.php';
if (file_exists($baseFile)) {
require_once $baseFile;
} else {
require_once 'PEAR/Config.php';
require_once PEAR_Config::singleton()
->get('horde_dir', null, 'pear.horde.org') . '/ingo/lib/Application.php';
}
Horde_Registry::appInit('ingo', array('cli' => true));
exit('Not updated');
// Initialize authentication manager.
$auth = $injector->getInstance('Horde_Auth')->getAuth();
// Make sure output is unbuffered.
ob_implicit_flush();
// Main loop.
$query = array();
while (!feof(STDIN)) {
$line = fgets(STDIN);
if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', trim($line), 2);
$query[$key] = $value;
} elseif ($line == "\n") {
if (empty($query['request']) || $query['request'] != 'smtpd_access_policy') {
Horde::log('Unrecognized request: ' . substr(var_export($query, true), 0, 200), 'ERR');
exit(1);
}
Horde::log(var_export($query, true), 'DEBUG');
$action = smtpd_access_policy($query);
Horde::log("Action: $action", 'DEBUG');
echo "action=$action\n\n";
@ob_flush();
$query = array();
} else {
Horde::log('Ignoring garbage: ' . substr($line, 0, 100), 'INFO');
}
}
exit(0);
/**
* Do policy checks
*
* @param array $query Query parameter hash
*
* @return string The access policy response.
*/
function smtpd_access_policy($query)
{
static $whitelists = array();
static $blacklists = array();
if (empty($query['sender']) || empty($query['recipient'])) {
return null;
}
// Try to determine the Horde username corresponding to the email recipient.
$user = $query['recipient'];
$pos = strpos($user, '@');
if ($pos !== false) {
$user = substr($user, 0, $pos);
}
try {
$user = $GLOBALS['injector']->getInstance('Horde_Core_Hooks')
->callHook('smtpd_access_policy_username', 'ingo', $query);
} catch (Horde_Exception_HookNotSet $e) {}
// Get $user's rules if we don't have them already.
if (!isset($whitelists[$user])) {
// Default empty rules.
$whitelists[$user] = array();
$blacklists[$user] = array();
// Retrieve the data.
$GLOBALS['auth']->setAuth($user, array());
$GLOBALS['session']->set('ingo', 'current_share', ':' . $user);
$ingo_storage = $GLOBALS['injector']->getInstance('Ingo_Factory_Storage')->create();
try {
$whitelists[$user] = $ingo_storage->retrieve(Ingo_Storage::ACTION_WHITELIST)->getWhitelist();
} catch (Ingo_Exception $e) {}
try {
$bl = $ingo_storage->retrieve(Ingo_Storage::ACTION_BLACKLIST);
if (!$bl->getBlacklistFolder()) {
// We will only reject email at delivery time if the user
// wants blacklisted mail deleted completely, not filed
// into a separate folder.
$blacklists[$user] = $bl->getBlacklist();
}
} catch (Ingo_Exception $e) {}
}
// Check whitelist rules first so that mistaken overlap doesn't
// result in lost mail.
if (in_array($query['sender'], $whitelists[$user])) {
return 'OK';
} elseif (in_array($query['sender'], $blacklists[$user])) {
return 'REJECT';
} else {
return 'DUNNO';
}
}