181 lines
5.6 KiB
PHP
Executable File
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';
|
|
}
|
|
}
|