#!/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';
    }
}
