#!/usr/bin/env php
<?php
/*
 * assblocker :: Continually process /var/log/mail.log and block assholes.
 *
 * IPs that fail dovecot auth are added as records to a table along with a timestamp.
 * These IPs are blocked at the firewall when added, and unblocked during the next
 * processing cycle after the record's TTL has expired.  Dovecot handles auth for
 * the SMTP, POP3, and IMAP4 services.  IPs that fail SMTP sessions have their session
 * examined to see if they failed due to specific errors that are blocked in the
 * configuration file. Matches due to session error message matches are permanently
 * blocked.  IPs belonging to Abuse-As-A-Service services and address verifiers, can
 * be blocked in the configuration file by domain names.  Specific IPs can be whitelisted
 * by either IP address or DNS name, also in the configuration file.
 *
 * Version 1.5.4, December 7, 2025
 * Copyright (c) 2025, Ron Guerin <ron@vnetworx.net>
 *
 * PHP Extensions: Required: PCRE, PCNTL
 * Suggests: tput, id
 *
 * Do not edit this script!  Edit /etc/${scriptname}.conf.php to
 * change settings, where ${scriptname} is the name of this file.
 * Changes made to the script will get overwritten on upgrades.
 *
 */

define('VERSION', '1.5.4');
define('MEPATH', realpath($argv[0]));
$basename = basename($argv[0]);
define('ME', preg_replace(chr(7).'\.php[0-9]*$'.chr(7), '', $basename));
restore_standard_timezone_policy();
define('VERSIONSTAMP', date('F j, Y H:i:s', stat(MEPATH)['mtime']));
openlog(ME, LOG_PID, LOG_USER); // Open syslog
cli_set_process_title(ME); // set proctitle

$MSG = $ERRORS = '';
$ERROR = $found = FALSE;

// Parse command-line, early
foreach ($argv as $index => $arg) {
	if (! $index) continue; // skip $argv[0]
	$argbase = ($pos = strpos($arg, '=')) ? substr($arg, 0, $pos) : $arg;
	switch ($argbase) {
		case '-V':
		case '--version':
			echo VERSION."\n";
			exit;
			break;
		case '?':
		case '-?':
		case '-h':
		case '--help':
		case 'help':
			help();
			exit;
			break;
		default:
			$found = TRUE;
			break;
	}
}
if (! $found) {
	help(TRUE);
	exit;
}

if (! function_exists('pcntl_fork')) {
	fwrite(STDERR, 'Error: '.ME.' requires the PHP PCNTL extension.'."\n");
	exit(2);
}

if (is_readable('/etc/'.ME.'.conf.php')) require '/etc/'.ME.'.conf.php';

define('DEBUG', (isset($debug) && $debug === TRUE) ? TRUE : FALSE);
define('PIDPATH', (isset($pidpath)) ? $pidpath : '/var/run');
define('ALLERRORS', '/var/log/allerrs/errors.log');
define('LOCKFILE', (isset($lockfile)) ? $lockfile : PIDPATH.'/'.ME.'.lock');
define('LOCKWAIT', (isset($proclockwait)) ? $proclockwait : 180); // seconds
define('LOGFILE', ((! isset($logfile)) || ($logfile === FALSE)) ? FALSE
	: (($logfile === TRUE) ? '/var/log/'.ME.'.log' : $logfile));
define('SYSLOG', ((! isset($syslog)) || (isset($syslog) && ($syslog !== FALSE))) ? TRUE : FALSE);
define('CYCLE', (isset($cycle)) ? $cycle : 5); // seconds
define('MAILFROM', (isset($mailfrom)) ? $mailfrom : ME.'@'.hostname(TRUE));
define('MAILFROMNAME', (isset($mailfromname)) ? $mailfromname : ME.' monitor service');
define('MAILTO', (isset($mailto)) ? $mailto : 'root@'.hostname(TRUE));
define('MAILTONAME', (isset($mailtoname)) ? $mailtoname : 'Mail Administrator');
define('MAILLOG', (isset($maillog)) ? $maillog : '/var/log/mail.log');
#Feb  1 23:55:29 orac postfix-mail/smtpd[1873177]: warning: syn-067-242-155-037.res.spectrum.com[67.242.155.37]: SASL PLAIN authentication failed: Connection lost to authentication server, sasl_username=(unavailable)
#Jan 23 23:56:32 orac postfix-mail/smtpd[3007052]: warning: unknown[121.147.65.227]: SASL LOGIN authentication failed: (reason unavailable), sasl_username=reasonable_drugs9@virtualalley.com
#2025-01-29T01:14:30.196843-05:00 inferno postfix-mail/smtpd[3007052]: warning: unknown[121.147.65.227]: SASL LOGIN authentication failed: (reason unavailable), sasl_username=reasonable_drugs9@virtualalley.com
#Feb  1 23:33:39 orac dovecot: auth-worker(332530): conn unix:auth-worker (pid=332529,uid=109): auth-worker<13229>: sql(wribbp@vnetworx.net,27.40.100.238): unknown user
#Feb 21 14:48:54 orac dovecot: auth-worker(332530): conn unix:auth-worker (pid=332529,uid=109): auth-worker<782025>: sql(thesupportservice@thesupportservice.com,117.114.194.219,<s+DXR6wuaJh1csLb>): unknown user
#Feb 21 14:48:56 orac dovecot: pop3-login: Disconnected: Connection closed (auth failed, 1 attempts in 2 secs): user=<thesupportservice@thesupportservice.com>, method=PLAIN, rip=117.114.194.219, lip=86.48.29.242, session=<s+DXR6wuaJh1csLb>
#Feb 21 17:27:31 orac dovecot: imap-login: Disconnected: Connection closed (auth failed, 3 attempts in 23 secs): user=<dwb1@vnetworx.net>, method=PLAIN, rip=111.10.209.160, lip=86.48.29.242, TLS, session=<w1C1fa4ugtdvCtGg>
#define('PFIX_TRAD', (isset($regextrad)) ? $pfixxtrad : '^([A-Za-z]+)\s+([0-9]+) ([0-9:]+) .* warning: .+\[(.*)\]: SASL .* authentication failed: (.*) sasl_username=(.*)$');
#define('PFIX_ISO', (isset($regexiso)) ? $pfixiso : '^([0-9T:.-]+) .* warning:.+\[(.*)\]: SASL .* authentication failed: (.*) sasl_username=(.*)$');
define('REGEX_TRAD', (isset($regextrad)) ? $regextrad : '^([A-Za-z]+)\s+([0-9]+) ([0-9:]+) .* dovecot: auth-worker\(.* sql\(([a-zA-Z0-9@.-]+),([0-9.]+)[,\)].*: (unknown user|Password mismatch)$');
define('REGEX_ISO', (isset($regexiso)) ? $regexiso : '^([0-9T:.-]+) .* dovecot: auth-worker\(.* sql\(([a-zA-Z0-9@.-]+),([0-9.]+)[,\)].*: (unknown user|Password mismatch)$');
// Mar  6 15:30:10 orac postfix-mail/smtpd[1110640]: connect from azpdwgbcz4fj.stretchoid.com[20.169.105.100]
// Feb 13 16:43:29 orac postfix-mail/smtpd[2030029]: connect from smtpout17.eu.briteverify.com[54.228.254.42]
define('REGEX_TRAD_PERM', (isset($regextradperm)) ? $regextradperm : '^([A-Za-z]+)\s+([0-9]+) ([0-9:]+) .+ .+/smtpd\[.*: connect from (.+){NAME}\[(.*)\]$');
define('REGEX_ISO_PERM', (isset($regextisoperm)) ? $regexisoperm : '^([0-9T:.-]+) .+ .+/smtpd\[.*: connect from (.+){NAME}\[(.*)\]$');
define('DEFAULT_TTL', 30); // days
$TTL = (isset($ttl)) ? $ttl : DEFAULT_TTL;
define('BLOCKLIST', (isset($blocklist)) ? $blocklist : '/etc/block.ips');
define('PERMANENT_BLOCKS', (isset($permanentblocks)) ? $permanentblocks : '/etc/rc.blocker');
define('BLOCKLISTNEW', substr(BLOCKLIST, 0, 1 + $pos = strrpos(BLOCKLIST, '/')).'.'.basename(BLOCKLIST));
// In firewall commands, {IP} is the placeholder for the IP address in the command
define('FW_BLOCK', (isset($block)) ? $block : '/usr/sbin/iptables -A INPUT -s {IP} -j DROP');
define('FW_UNBLOCK', (isset($unblock)) ? $unblock : '/usr/sbin/iptables -D INPUT -s {IP} -j DROP >/dev/null 2>&1');
define('ERRORMAILDIR', (isset($errormaildir)) ? $errormaildir : FALSE);
$GLOBALS['EXEMPT'] = (isset($exempt) && is_array($exempt)) ? $exempt : array();
$GLOBALS['PERMANENT'] = (isset($permanent) && is_array($permanent)) ? $permanent : array();
$GLOBALS['MAILBLOCK'] = (isset($mailblock) && is_array($mailblock)) ? $mailblock : array();
if (LOGFILE && (! is_dir(dirname(LOGFILE)))) mkdir(dirname(LOGFILE), 0700, TRUE);

$error = $skip = $found = $run = $daemonize = $stop = $restart = $reload = $permblock = $type = $name
	= $expire = $unblock = $unblockall = $update = $show = $stats = $exclusive = $exclusives = $both = FALSE;
$exclusiveslist = '';
$cmdlineips = array();
// Parse command-line, later
foreach ($argv as $index => $arg) {
	if (! $index) continue; // skip $argv[0]
	if ($skip) {
		$skip = FALSE;
		continue;
	}
	$argbase = ($pos = strpos($arg, '=')) ? substr($arg, 0, $pos) : $arg;
	switch ($argbase) {
		case '-r':
		case '--run':
			$run = TRUE;
			$found = TRUE;
			$exclusiveslist .= $argbase.' ';
			if ($exclusive) $exclusives = TRUE;
			$exclusive = TRUE;
			break;
		case '-d':
		case '--daemonize':
			$daemonize = TRUE;
			$found = TRUE;
			$exclusiveslist .= $argbase.' ';
			if ($exclusive) $exclusives = TRUE;
			$exclusive = TRUE;
			break;
		case '-R':
		case '--restart':
			$restart = TRUE;
			$found = TRUE;
			$exclusiveslist .= $argbase.' ';
			if ($exclusive) $exclusives = TRUE;
			$exclusive = TRUE;
			break;
		case '-L':
		case '--reload':
			$reload = TRUE;
			$found = TRUE;
			$exclusiveslist .= $argbase.' ';
			if ($exclusive) $exclusives = TRUE;
			$exclusive = TRUE;
			break;
		case '-S':
		case '--stop':
			$stop = TRUE;
			$found = TRUE;
			$exclusiveslist .= $argbase.' ';
			if ($exclusive) $exclusives = TRUE;
			$exclusive = TRUE;
			break;
		case '-b':
		case '--both':
			$both = TRUE;
			break;
		case '-p':
		case '--permanent':
			$permblock = TRUE;
			break;
		case '-e':
		case '--expire':
			$expire = TRUE;
			$found = TRUE;
			$exclusiveslist .= $argbase.' ';
			if ($exclusive) $exclusives = TRUE;
			$exclusive = TRUE;
			break;
		case '-U':
		case '--unblockall':
			$unblockall = TRUE;
			$found = TRUE;
			$exclusiveslist .= $argbase.' ';
			if ($exclusive) $exclusives = TRUE;
			$exclusive = TRUE;
			break;
		case '-w':
		case '--show':
			$show = TRUE;
			$found = TRUE;
			$exclusiveslist .= $argbase.' ';
			if ($exclusive) $exclusives = TRUE;
			$exclusive = TRUE;
			break;
		case '-s':
		case '--stats':
			$stats = TRUE;
			$found = TRUE;
			$exclusiveslist .= $argbase.' ';
			if ($exclusive) $exclusives = TRUE;
			$exclusive = TRUE;
			break;
		case '-u':
		case '--unblock':
			$unblock = TRUE;
			$found = TRUE;
			$exclusiveslist .= $argbase.' ';
			if ($exclusive) $exclusives = TRUE;
			$exclusive = TRUE;
			if ((! array_key_exists($index+1, $argv)) || (substr($argv[$index+1], 0, 1) == '-')) {
				@fwrite(STDERR, 'Error: No IP given for '.$argbase."\n");
				$error = TRUE;
			}
			break;
		case '-t':
		case '--type':
			$found = TRUE;
			if ((! array_key_exists($index+1, $argv)) || (substr($argv[$index+1], 0, 1) == '-')) {
				@fwrite(STDERR, 'Error: No type (u, a, v) given for '.$argbase."\n");
				$error = TRUE;
			}
			$type = $argv[$index+1];
			if (($type != 'u') && ($type != 'a') && ($type != 'v')) {
				@fwrite(STDERR, 'Error: Type given must be \'u\', \'a\', or \'v\' given for '.$argbase."\n");
				$error = TRUE;
			}
			break;
		case '-n':
		case '--name':
			$found = TRUE;
			if ((! array_key_exists($index+1, $argv)) || (substr($argv[$index+1], 0, 1) == '-')) {
				@fwrite(STDERR, 'Error: No name given for '.$argbase."\n");
				$error = TRUE;
			}
			$name = $argv[$index+1];
			break;
		default:
			if (substr($arg, 0, 1) == '-') {
				@fwrite(STDERR, 'Error: Unknown argument "'.$argbase.'"'."\n");
				$error = TRUE;
			}
			if (! substr_count($arg, '.')) {
				@fwrite(STDERR, 'Error: Invalid IPv4 address "'.$argbase.'"'."\n");
				$error = TRUE;
			}
			$cmdlineips[] = $argbase;
			$found = TRUE;
			break;
	}
}
if (! $found) {
	@fwrite(STDERR, 'Error: Valid action option or IP address must be specified.'."\n");
	$error = TRUE;
}
if ($exclusives || ($exclusive && count($cmdlineips) && (! $unblock))) {
	$msg = 'Error: Cannot specify together '.rtrim($exclusiveslist);
	if (count($cmdlineips)) $msg .= ' and IPs';
	@fwrite(STDERR, $msg."\n");
	$error = TRUE;
}
if ((posix_getuid() != 0) && ((! $show) && (! $stats))) {
	@fwrite(STDERR, 'Error: You must be root to run this command'."\n");
	$error = TRUE;
}
if ((! $permblock) && ($type || $name)) {
	if ($type) @fwrite(STDERR, 'Error: Type is only valid for --permanent'."\n");
	if ($name) @fwrite(STDERR, 'Error: Name is only valid for --permanent'."\n");
	$error = TRUE;
}
if ((! $unblock) && $both) {
	@fwrite(STDERR, 'Error: --both is only valid for --unblock'."\n");
	$error = TRUE;
}
if ($error) exit(1);

if (! function_exists('str_contains')) { // polyfill for PHP < 8
	function str_contains($haystack, $needle) {
		if ($needle == '') return TRUE;
		return (($needle !== '') && (mb_strpos($haystack, $needle) !== FALSE));
	}
}

// Load permanent and temporary blocklists
if (! file_exists(PERMANENT_BLOCKS)) {
	$omask = decoct(umask(0077));
	touch(PERMANENT_BLOCKS);
	chmod(PERMANENT_BLOCKS, 0700);
	umask($omask);
}
$permips = blocklist_load_permanent(); // Must load permanent blocks before temporary blocks

if (! file_exists(BLOCKLIST)) {
	$omask = decoct(umask(0077));
	touch(BLOCKLIST);
	umask($omask);
}
$ips = blocklist_load($permips);

// Execute the given commandline
if (count($cmdlineips) && (! $unblock)) { // Block IPs given on commandline
	$update = FALSE;
	foreach ($cmdlineips as $ip) { // block or update timestamp
		$ip = trim($ip);
		$already = FALSE;
		if ($permblock) {
			if (ip_block_permanent($permips, $ips, $ip, time(), $type, $name)) { // block
				$msg = 'Manually permanently blocked ['.$ip.'] at '.date('F j, Y H:i:s');;
				if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
				syslog(LOG_MAIL|LOG_INFO, $msg);
				logfile($msg);
			}
			else exit(1);
		}
		else { // TTL-based blocking
			if (! array_key_exists($ip, $ips)) {
				if (ip_block($permips, $ips, $ip)) { // block
					$msg = 'Manually blocked ['.$ip.'] at '.date('F j, Y H:i:s', $ips[$ip]);
					if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
					syslog(LOG_MAIL|LOG_INFO, $msg);
					logfile($msg);
					$update = TRUE;
				}
				else { // $time is FALSE because of an error calling ip_block($ip)
					fwrite(STDERR, 'Error: Unable to block ['.$ip.']'."\n");
					exit(1);
				}
			}
			else { // update timestamp
				$ips[$ip] = time();
				$update = TRUE;
			}
		}
	}
	$expired = ip_expire($ips); // $ips passed by reference
	if (($update || $expired) && (! blocklist_write($ips))) {
		fwrite(STDERR, 'Error: Unable to write '.BLOCKLIST."\n");
		exit(1);
	}
	signal_send(SIGHUP, TRUE); // tell running monitor to reload
}
elseif ($stats) { // display stats
	$ctr = 0;
	if (! file_exists(BLOCKLIST)) $ctr = 0;
	else foreach (file(BLOCKLIST, FILE_IGNORE_NEW_LINES) as $line) $ctr++;
	echo 'TTL: '.$TTL.' days, '.$ctr.' IP addresses currently blocked.'."\n";
	exit;
}
elseif ($show) { // show blocked IPs
	$ctr = 0;
	if (file_exists(BLOCKLIST)) foreach (file(BLOCKLIST, FILE_IGNORE_NEW_LINES) as $line) {
		$ctr++;
		$pos = strpos($line, ' ');
		$ip = substr($line, 0, $pos);
		$stamp = substr($line, $pos + 1);
		echo 'IP: '.$ip.'  Blocked: '.date('F j, Y H:i:s', $stamp)."\n";
	}
	echo '---------------------------------------------------------'."\n";
	echo $ctr.' IP addresses currently blocked.'."\n";
	exit;
}
elseif ($stop) { // stop --run or --daemonized instance
	// send SIGTERM to running version of ME
	if (! signal_send(SIGTERM)) {
		$msg = 'Error: Unable to stop '.ME;
		fwrite(STDERR, $msg."\n");
		syslog(LOG_MAIL|LOG_INFO, $msg);
		exit(1);
	}
	exit;
}
elseif ($restart) { // send SIGUSR1 to running version of ME
	if (! signal_send(SIGUSR1)) {
		$msg = 'Error: Unable to restart '.ME;
		fwrite(STDERR, $msg."\n");
		syslog(LOG_MAIL|LOG_INFO, $msg);
		exit(1);
	}
	exit;
}
elseif ($reload) { // send SIGHUP to running version of ME
	if (! signal_send(SIGHUP)) {
		$msg = 'Error: Unable to reload '.ME;
		fwrite(STDERR, $msg."\n");
		syslog(LOG_MAIL|LOG_INFO, $msg);
		exit(1);
	}
	exit;
}
elseif ($unblock) { // Unblock IP(s) on commandline
	if ($permblock || $both) {
		$needreload = FALSE;
		foreach ($cmdlineips as $ip) {
			if (! in_array($ip, $permips)) {
				fwrite(STDERR, 'Warning: ['.$ip.'] was not permanently blocked, so could not be unblocked permanent.'."\n");
			}
			else {
				if (ip_unblock_permanent($permips, $ip)) {
					$needreload = TRUE;
					$msg = 'Manually unblocked permanent ['.$ip.'] at '.date('F j, Y H:i:s');;
					if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
					syslog(LOG_MAIL|LOG_INFO, $msg);
					logfile($msg);
				}
			}
		}
		if ($needreload) signal_send(SIGHUP, TRUE); // tell running monitor to reload
	}
	if ((! $permblock) || $both) {
		$update = FALSE;
		foreach (file(BLOCKLIST, FILE_IGNORE_NEW_LINES) as $line)
			$ips[substr($line, 0, $pos = strpos($line, ' '))] = substr($line, $pos + 1);
		foreach ($cmdlineips as $ip) {
			if (! array_key_exists($ip, $ips)) {
				fwrite(STDERR, 'Warning: ['.$ip.'] was not blocked, so could not be unblocked.'."\n");
			}
			else {
				if (ip_unblock($ips, $ip)) {
					$msg = 'Manually unblocked ['.$ip.'] at '.date('F j, Y H:i:s');;
					if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
					syslog(LOG_MAIL|LOG_INFO, $msg);
					logfile($msg);
					$update = TRUE;
				}
				else fwrite(STDERR, 'Error: Unable to unblock ['.$ip.']'."\n");
			}
		}
		$expired = ip_expire($ips); // $ips passed by reference
		if ($update || $expired) {
			if (! blocklist_write($ips)) {
				fwrite(STDERR, 'Error: Unable to write '.BLOCKLIST."\n");
				exit(1);
			}
			signal_send(SIGHUP, TRUE); // tell running monitor to reload
		}
	}
}
elseif ($unblockall) { // unblock all blocked IPs
	if ($permblock) {
		$msg = 'Unblocking all permanently blocked ranges not supported.';
		fwrite(STDERR, $msg."\n");
		syslog(LOG_MAIL|LOG_INFO, $msg);
		exit(1);
	}
	$local = TRUE; // local command
	if (! ($ips = blocklist_apply($permips, $ips, TRUE))) { // TRUE=unblockall
		$msg = 'Unable to read/unblock all '.ME.' blocklist: '.BLOCKLIST;
		fwrite(STDERR, $msg."\n");
		syslog(LOG_MAIL|LOG_INFO, $msg);
		exit(1);
	}
	else {
		if (! blocklist_write($ips)) {
			$msg = 'Unable to write '.ME.' blocklist: '.BLOCKLIST;
			fwrite(STDERR, $msg."\n");
			syslog(LOG_MAIL|LOG_INFO, $msg);
			exit(1);
		}
		signal_send(SIGHUP, TRUE); // tell running monitor to reload
	}
	exit;
}
elseif ($expire) { // Unblock records whose TTL has expired
	$local = TRUE; // local command
	$ips = array();
	if (! file_exists(BLOCKLIST)) touch(BLOCKLIST);
	foreach (file(BLOCKLIST, FILE_IGNORE_NEW_LINES) as $line)
		$ips[substr($line, 0, $pos = strpos($line, ' '))] = substr($line, $pos + 1);
	$expired = ip_expire($ips); // $ips passed by reference
	if ($expired) {
		if (! blocklist_write($ips)) {
			fwrite(STDERR, 'Error: Unable to write '.BLOCKLIST."\n");
			exit(1);
		}
		signal_send(SIGHUP, TRUE); // tell running monitor to reload
	}
}
elseif ($run || $daemonize) { // start monitoring logfile
	if (! ($LOCK = (($daemonize) ? daemonize() : lock_pid()))) { // daemonize this instance or run in foreground
		$msg = ME.' already running or unable to obtain lock.';
		error($msg, LOG_MAIL|LOG_INFO);
		exit(1);
	}
	pcntl_signal(SIGINT,  'signal_handler');    // terminate
	pcntl_signal(SIGTERM, 'signal_handler');    // terminate
	pcntl_signal(SIGHUP,  'signal_handler');    // reload
	pcntl_signal(SIGUSR1, 'signal_handler');    // restart
	pcntl_async_signals(TRUE);                  // process signals immediately
	register_shutdown_function('shutdown');

	$lastsize = NULL; // NULL starts reading from beginning of file
	$gccycle = $inode = $waiting = 0;

	$msg = 'Starting '.ME.' '.VERSION.' at '.date('F j, Y H:i:s');
	if ($run && DEBUG) output($msg);
	else syslog(LOG_MAIL|LOG_INFO, $msg);

	if (FALSE === ($ips = blocklist_apply($permips, $ips))) {
		$msg = 'Unable to read '.ME.' blocklist: '.BLOCKLIST;
		error($msg, LOG_MAIL|LOG_INFO);
		exit(1);
	}

	if (! blocklist_write($ips)) {
		$msg = 'Error: Unable to write '.BLOCKLIST;
		error($msg, LOG_MAIL|LOG_INFO);
		exit(1);
	}

	// At startup, read from start of file ($lastsize = NULL), be sure to account for things
	// that have already been blocked.  Then loop forever through new log data.
	while (TRUE) {
		$updatet = $updatei = $updatem = $reason = $expired = FALSE;
		pcntl_signal_dispatch();
		if (++$gccycle == 100) {
			$gccycle = 0;
			gc_collect_cycles();
		}
		clearstatcache();
		if (ERRORMAILDIR) foreach (glob(ERRORMAILDIR.'/new/*') as $mailfile) {
			// Check any SMTP error emails for blockable conditions
			$ip = $reason = $servername = '';
			$blockperm = FALSE;
			$mail = file_get_contents($mailfile);
			// Subject: Postfix SMTP server: errors from unknown[107.173.189.10]
			if (preg_match(chr(7).'^Subject: Postfix SMTP server: errors from (.*)\[(.*)\]$'.chr(7).'m', $mail, $matches)) {
				$servername = $matches[1];
				$ip = $matches[2];
				if (in_array($ip, $permips)) continue;
				if (preg_match(chr(7).'^Date: (.*)$'.chr(7).'m', $mail, $matches)) {
					$stamp = strtotime($matches[1]);
					foreach ($PERMANENT as $map) { // Check to see if we should permanently block this IP
						if (strpos($servername, $map['domain'])) {
							$blockperm = TRUE;
							ip_block_permanent($permips, $ips, $ip, $stamp, $map['type'], $servername);
						}
					}
					if ((! $blockperm) && (! array_key_exists($ip, $ips))) {
						// Look for objectionable failures.
						foreach ($MAILBLOCK as $name => $errline) if (strpos($mail, $errline)) $reason = $name;
					}
				}
			}
			if ($reason) $updatem = ip_evaluate($permips, $ips, $ip, $stamp, $servername, $reason, $lastsize, 1);
			if (DEBUG) rename($mailfile, ERRORMAILDIR.'/cur/'.basename($mailfile));
			else unlink($mailfile);
		}
		$stat = stat(MAILLOG);
		if (file_exists(MAILLOG) && (($stat['size'] != $lastsize) || ($inode != $stat['ino']))) {
			// inode or logfile size has changed
			$data = '';
			$fd = fopen(MAILLOG, 'r');
			// Better, I think, to seek forward than back, for accuracy sake
			//fseek($fd, ($lastsize - $stat['size']) - 1, SEEK_END);
			fseek($fd, (int) $lastsize, SEEK_SET);
			while ((! feof($fd)) && (($lastsize > 0) || ($lastsize === NULL))) $data .= fgets($fd, 1024);
			fclose($fd);
			$inode = $stat['ino'];
			// To prevent double-blocking, handle permanent blocks first
			foreach ($PERMANENT as $map) { // Check to see if we should permanently block this IP
				$regname = str_replace('.', '\.', $map['domain']);
				$regextrad = str_replace('{NAME}', $regname, REGEX_TRAD_PERM);
				$regexiso = str_replace('{NAME}', $regname, REGEX_ISO_PERM);
				if (preg_match_all(chr(7).$regextrad.chr(7).'miU', $data, $matches) != FALSE) {
					// Jan 23 00:01:00 Determine year by comparing to current month
					foreach ($matches[5] as $key => $ip) { // loop through matches
						if (in_array($ip, $permips)) continue;
						$hostname = $matches[4][$key];
						$name = $hostname.$map['domain'];
						$mon = $matches[1][$key];
						$day = (strlen($matches[2][$key]) == 1) ? '0'.$matches[2][$key] : $matches[2][$key];
						$year = ((($mon == 'Dec') || ($mon == 'Nov') || ($mon == 'Oct'))
							&& ((date('m') == 1) || (date('m') == 2) || (date('m') == 3))) ? date('Y') - 1 : date('Y');
						$time = $matches[3][$key];
						$stampstr = $year.'-'.$mon.'-'.$day.' '.$time.' '.date('O');
						$stamp = strtotime($year.'-'.$mon.'-'.$day.' '.$time.' '.date('O'));
						ip_block_permanent($permips, $ips, $ip, $stamp, $map['type'], $name);
					}
				}
				if (preg_match_all(chr(7).$regexiso.chr(7).'miU', $data, $matches) != FALSE) {
					// 2025-01-29T01:14:30.196843-05:00 inferno
					foreach ($matches[3] as $key => $ip) { // loop through matches
						if (in_array($ip, $permips)) continue;
						$hostname = $matches[2][$key];
						$name = $hostname.$map['name'];
						$stamp = strtotime($matches[1][$key]);
						ip_block_permanent($permips, $ips, $ip, $stamp, $map['type'], $name);
					}
				}
			}
			// Look for all matches in this chunk of data
			if (preg_match_all(chr(7).REGEX_TRAD.chr(7).'miU', $data, $matches) != FALSE) {
				// Jan 23 00:01:00 Determine year by comparing to current month
				foreach ($matches[5] as $key => $ip) { // loop through matches
					if (in_array($ip, $permips) || array_key_exists($ip, $ips)) continue;
					$user = $matches[4][$key];
					$reason = $matches[6][$key];
					$mon = $matches[1][$key];
					$day = (strlen($matches[2][$key]) == 1) ? '0'.$matches[2][$key] : $matches[2][$key];
					$year = ((($mon == 'Dec') || ($mon == 'Nov') || ($mon == 'Oct'))
						&& ((date('m') == 1) || (date('m') == 2) || (date('m') == 3))) ? date('Y') - 1 : date('Y');
					$time = $matches[3][$key];
					$stampstr = $year.'-'.$mon.'-'.$day.' '.$time.' '.date('O');
					$stamp = strtotime($year.'-'.$mon.'-'.$day.' '.$time.' '.date('O'));
					$updatet = ip_evaluate($permips, $ips, $ip, $stamp, $user, $reason, $lastsize);
				}
			}
			if (preg_match_all(chr(7).REGEX_ISO.chr(7).'miU', $data, $matches) != FALSE) {
				// 2025-01-29T01:14:30.196843-05:00 inferno
				foreach ($matches[3] as $key => $ip) { // loop through matches
					if (in_array($ip, $permips) || array_key_exists($ip, $ips)) continue;
					$reason = $matches[4][$key];
					$stamp = strtotime($matches[1][$key]);
					$updatei = ip_evaluate($permips, $ips, $ip, $stamp, $user, $reason, $lastsize);
				}
			}
			$lastsize = $stat['size']; // change $lastsize only at processing end so to send NULL first time through
		} // inode or logfile size had changed

		// If something's TTL expired, unblock it
		$expired = ip_expire($ips); // $ips passed by reference

		// If any cause blocked or expired something, re-write the changed blocklist file
		if (($updatet || $updatei || $updatem || $expired) && (! blocklist_write($ips))) {
			error('Error: Unable to write '.BLOCKLIST, LOG_MAIL|LOG_INFO);
			exit(1);
		}

		pcntl_signal_dispatch();
		sleep (CYCLE);
	} // while (TRUE)
} // run or daemonize

exit;


####################################################################################################################################
####################################################################################################################################


function shutdown($sigint=FALSE) {
	if ($GLOBALS['ERROR'] || ($GLOBALS['MSG'] && DEBUG))
		mail_report(MAILFROM, MAILTO, ME.' issues', $GLOBALS['MSG'], FALSE, MAILFROMNAME, MAILTONAME);
	if (array_key_exists('LOCK', $GLOBALS)) {
		ftruncate($GLOBALS['LOCK'], 0);
		unlock($GLOBALS['LOCK']);
	}
	$msg = 'Shutting down '.ME.' at '.date('F j, Y H:i:s');
	if ($GLOBALS['run'] && DEBUG) output($msg);
	else syslog(LOG_MAIL|LOG_INFO, $msg);
	closelog();
}

function ip_evaluate($permips, &$ips, $ip, $stamp, $user, $reason, $lastsize, $st=0) {
	// Evaluates an IP to determine if it should be temporarily blocked.
	// Blocks or doesn't block IP as appropriate.
	// Returns TRUE if IP is blocked, FALSE if not.
	// $st: Sourcetype 0-log, 1-mail
	$update = $ignore = FALSE;
	$ip = trim($ip);
	$reason = strtolower($reason);
	if (in_array($ip, $permips)) return FALSE;
	if (! array_key_exists($ip, $ips)) {
		if (($stamp + ($GLOBALS['TTL'] * 86400)) > time()) { // Block if timestamp + TTL days is still in the future
			foreach ($GLOBALS['EXEMPT'] as $exempt) {
				$ignore = FALSE;
				if ((filter_var($exempt, FILTER_VALIDATE_IP) === FALSE) && ($records = dns_get_record($exempt, DNS_A))) {
					foreach ($records as $data) {
						if ($data['ip'] == $ip) {
							$ip = $exempt;
							$ignore = TRUE;
							break 2;
						}
					}
				}
				elseif ($ip == $exempt) {
					$ignore = TRUE;
					break;
				}
			}
			if ($ignore) {
				$msg = 'Ignoring '.$reason.' for '.$user.' from exempt address ['.$ip.'] at '.date('F j, Y H:i:s', $stamp);
				if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
				syslog(LOG_MAIL|LOG_INFO, $msg);
				logfile($msg);
				if ($GLOBALS['run'] && DEBUG) echo $msg."\n";
			}
			elseif (ip_block($permips, $ips, $ip, $stamp)) {
				$update = TRUE;
				$msg = 'Blocking ['.$ip.'] due to '.$reason.' for '.$user.' at '.date('F j, Y H:i:s', $stamp);
				if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
				syslog(LOG_MAIL|LOG_INFO, $msg);
				logfile($msg);
				if ($GLOBALS['run'] && DEBUG) echo $msg."\n";
			}
			elseif (! $st) {
				$msg = 'Error: Unable to block ['.$ip.']';
				if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
				syslog(LOG_MAIL|LOG_INFO, $msg);
				logfile($msg);
				if ($GLOBALS['run'] && DEBUG) echo $msg."\n";
			}
		}
	} // if (! array_key_exists($ip, $ips))
	elseif (($lastsize !== NULL) && (! $st)) { // ignore warning during initial load and mail inputs
		$msg = 'Warning: Cannot block, IP ['.$ip.'] is already blocked, at '.date('F j, Y H:i:s', $stamp);
		if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
		syslog(LOG_MAIL|LOG_INFO, $msg);
		logfile($msg);
		if ($GLOBALS['run'] && DEBUG) echo $msg."\n";
	}
	return $update;
}

function ip_block($permips, &$ips, $ip, $stamp=FALSE) {
	// Blocks $ip at firewall
	// iptables -A INPUT -s 111.70.23.253 -j DROP
	if ((in_array($ip, $permips)) || (array_key_exists($ip, $ips))) return FALSE;
	if ($stamp === FALSE) $stamp = time();
	$command = str_replace('{IP}', $ip, FW_BLOCK);
	if (DEBUG) {
		$msg = 'bcommand: '.$command;
		$msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
		syslog(LOG_MAIL|LOG_INFO, $msg);
		if ($GLOBALS['run'] && DEBUG) echo $msg."\n";
	}
	if ((FALSE === exec($command, $out, $code)) || $code) {
		error('Error: Unable to run firewall command: '.$command, LOG_MAIL|LOG_INFO);
		return FALSE;
	}
	$ips[$ip] = $stamp;
	return TRUE;
}

function ip_block_permanent(&$permips, &$ips, $ip, $stamp, $type='u', $name='', $st=0) {
	// Blocks $ip at firewall, returns TRUE if IP is blocked, FALSE if fails to block
	// iptables -A INPUT -s 111.70.23.253 -j DROP
	$parts = explode('.', $name);
	$count = count($parts);
	$domain = ($name) ? $parts[$count - 2].'.'.$parts[$count - 1] : '';
	if (! $type) $type = 'u';
	if (array_key_exists($ip, $ips)) ip_unblock($ips, $ip); // Undo temporary block
	if (! in_array($ip, $permips)) {
		$command = str_replace(array('{IP}', ' >/dev/null 2>&1'), array($ip, ''), FW_BLOCK);
		if (DEBUG) {
			$msg = 'permcommand: '.$command;
			$msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
			syslog(LOG_MAIL|LOG_INFO, $msg);
			if ($GLOBALS['run'] && DEBUG) echo $msg."\n";
		}
		if ((FALSE === exec($command, $out, $code)) || $code) {
			error('Error: Unable to run firewall command: '.$command, LOG_MAIL|LOG_INFO);
			return FALSE;
		}
		else {
			$reason = '';
			if ($type == 'a') $reason = 'attacker ';
			elseif ($type == 'v') $reason = 'verifier ';
			$msg = 'Permanently blocking '.$reason.'['.$ip.'] due to '.$domain.' at '.date('F j, Y H:i:s', $stamp);
			if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
			syslog(LOG_MAIL|LOG_INFO, $msg);
			logfile($msg);
			if ($GLOBALS['run'] && DEBUG) echo $msg."\n";
			if (DEBUG) {
				$msg = 'writeout: '.$command.' # '.$stamp.' '.$type.(($name) ? ' '.$name : '');
				$msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
				syslog(LOG_MAIL|LOG_INFO, $msg);
				if ($GLOBALS['run'] && DEBUG) echo $msg."\n";
			}
		}
		if (! file_put_contents(PERMANENT_BLOCKS, $command.' # '.$stamp.' '.$type
			.(($name) ? ' '.$name : '')."\n", FILE_APPEND|LOCK_EX)) {
			error('Error: Unable to write: "'.$command.'" to '.PERMANENT_BLOCKS, LOG_MAIL|LOG_INFO);
			return FALSE;
		}
		$permips[] = $ip;
		return TRUE;
	}
	if (! $st) error('Error: IP is already permanently blocked: '.$ip, LOG_MAIL|LOG_INFO);
	return FALSE;
}

function ip_unblock(&$ips, $ip) {
	// Unblocks $ip at firewall
	// iptables -D INPUT -s 111.70.23.253 -j DROP
	$command = str_replace('{IP}', $ip, FW_UNBLOCK);
	// Trying to unblock something that isn't blocked generates error
	if (((FALSE === ($line = exec($command, $out, $code))) || $code)
		&& (! str_contains($line, 'does a matching rule exist in that chain?'))) return FALSE;
	unset($ips[$ip]);
	return TRUE;
}

function ip_unblock_permanent(&$permips, $ip) {
	// Unblocks $ip at firewall
	// iptables -D INPUT -s 111.70.23.253 -j DROP
	if (! in_array($ip, $permips)) {
		error('Error: IP ['.$ip.'] is not permanently blocked.', LOG_MAIL|LOG_INFO);
		return FALSE;
	}
	foreach ($permips as $key => $ipa) {
		if ($ipa == $ip) array_slice($permips, $key, 1);
	}
	$command = str_replace('{IP}', $ip, FW_UNBLOCK);
	// Trying to unblock something that isn't blocked generates error
	if (((FALSE === ($line = exec($command, $out, $code))) || $code)
		&& (! str_contains($line, 'does a matching rule exist in that chain?'))) return FALSE;
	$new = array();
	foreach (file(PERMANENT_BLOCKS) as $line) if (! strpos($line, $ip)) $new[] = $line;
	if (FALSE === file_put_contents(PERMANENT_BLOCKS, $new, LOCK_EX)) {
		error('Error: Unable to write: '.PERMANENT_BLOCKS, LOG_MAIL|LOG_INFO);
		return FALSE;
	}
	$msg = 'Unblocked permanent ['.$ip.'] at '.date('F j, Y H:i:s');;
	if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
	syslog(LOG_MAIL|LOG_INFO, $msg);
	logfile($msg);
	return TRUE;
}

function ip_expire(&$ips) {
	// Removes exempt and expired records and unblocks the associated IPs
	$update = FALSE;
	foreach ($GLOBALS['EXEMPT'] as $exempt) {
		if ((filter_var($exempt, FILTER_VALIDATE_IP) === FALSE) && ($records = dns_get_record($exempt, DNS_A))) {
			foreach ($records as $data) {
				if (in_array($data['ip'], $ips) && (ip_unblock($ips, $data['ip']))) {
					$msg = 'Exempt, unblocking ['.$exempt.'] at '.date('F j, Y H:i:s');;
					if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
					syslog(LOG_MAIL|LOG_INFO, $msg);
					logfile($msg);
					if ($GLOBALS['run'] && DEBUG) echo $msg."\n";
					$update = TRUE;
				}
			}
		}
	}
	$blockfor = $GLOBALS['TTL'] * 86400;
	foreach ($ips as $ip => $blocktime) {
		if (($blocktime + $blockfor) < time()) { // unblock
			if (ip_unblock($ips, $ip)) {
				$msg = 'TTL expired, unblocking ['.$ip.'] from '.date('F j, Y H:i:s', $blocktime).' at '.date('F j, Y H:i:s');
				if (DEBUG) $msg .= ' mem:'.memory_get_usage().' max:'.memory_get_peak_usage();
				syslog(LOG_MAIL|LOG_INFO, $msg);
				logfile($msg);
				if ($GLOBALS['run'] && DEBUG) echo $msg."\n";
				$update = TRUE;
			}
		}
	}
	return $update;
}

function blocklist_load_permanent() {
	// Reads permanent blocklist into array to avoid duplication
	$match = str_replace(array('{IP}', ' >/dev/null 2>&1'), array('(.+)', ''), FW_BLOCK);
	foreach (file(PERMANENT_BLOCKS, FILE_IGNORE_NEW_LINES) as $line) {
		if (preg_match(chr(7).$match.chr(7), $line, $matches)) $permips[] = $matches[1];
	}
	return $permips;
}

function blocklist_load($permips) {
	// Read stored list, returns array of $ips[$ip]=$stamp
	$ips = array();
	if (! file_exists(BLOCKLIST)) touch(BLOCKLIST);
	foreach (file(BLOCKLIST, FILE_IGNORE_NEW_LINES) as $line) {
		$pos = strpos($line, ' ');
		$ip = substr($line, 0, $pos);
		$stamp = substr($line, $pos + 1);
		if ((in_array($ip, $permips)) || (array_key_exists($ip, $ips))) continue;
		$ips[$ip] = $stamp;
	}
	return $ips;
}

function blocklist_apply($permips, $ips, $removeall=FALSE) {
	// Firewalls array of IPs, or removes all IPs from firewall
	$ctr = 0;
	$commands = '';
	foreach ($ips as $ip => $stamp) {
		$out = array(); $code = 0;
		// iptables -D INPUT -s 111.70.23.253 -j DROP
		$commands .= str_replace('{IP}', $ip, FW_UNBLOCK).';'; // Remove everything in list so we don't add anything twice
		if ((! $removeall) && (($stamp + ($GLOBALS['TTL'] * 86400)) > time())
			&& (! in_array($ip, $permips))) { // Add blocking rule if not $removeall and not in $permips
			// iptables -A INPUT -s 111.70.23.253 -j DROP
			$commands .= str_replace('{IP}', $ip, FW_BLOCK).';';
		}
		if ($removeall || in_array($ip, $permips)) unset($ips[$ip]);
		if (++$ctr == 100) { // Do these in batches of 100
			// Trying to unblock something that isn't blocked generates error, so we have to ignore errors here. :(
			exec($commands);
			$ctr = 0;
			$commands = '';
		}
	}
	if ($commands) exec($commands); // Do last batch of commands
	return $ips;
}

function blocklist_write($ips) {
	// Writes the blocklist to a file
	if (! ($lock = lock_exclusive(BLOCKLIST, 120))) {
		error('Error: Could not obtain exclusive lock on: '.BLOCKLIST.' in '.__FUNCTION__, LOG_MAIL|LOG_INFO);
		return FALSE;
	}

	if (! ($handle = fopen(BLOCKLISTNEW, 'w'))) {
		error('Cannot open file: '.BLOCKLISTNEW, LOG_MAIL|LOG_INFO);
		unlock($lock);
		return FALSE;
	}

	foreach ($ips as $ip => $blocktime) {
		if (fwrite($handle, $ip.' '.$blocktime."\n") === FALSE) {
			error('Cannot write to file: '.BLOCKLISTNEW, LOG_MAIL|LOG_INFO);
			fclose($handle);
			unlock($lock);
			return FALSE;
		}
	}
	fclose($handle);
	rename(BLOCKLISTNEW, BLOCKLIST);
	unlock($lock);
	return TRUE;
}

function restart() {
	syslog(LOG_MAIL|LOG_INFO, 'Restarting '.ME.' at '.date('F j, Y H:i:s'));
	$args = array();
	foreach ($GLOBALS['argv'] as $key => $arg) {
		if (! $key) continue;
		$args[] = $arg;
	}
	shutdown();
	pcntl_exec($GLOBALS['argv'][0], $args); // remember $argv[0] will *not* be the SUID wrapper (if used)
	exit(1); // should never get here after a pcntl_exec()
}

function reload() {
	$pid = (int) trim(file_get_contents(LOCKFILE));
	if ($pid) {
		$msg = 'Reloading '.ME.' at '.date('F j, Y H:i:s');
		syslog(LOG_MAIL|LOG_INFO, $msg);
		if ($GLOBALS['run'] && DEBUG) echo $msg."\n";
		if (is_readable('/etc/'.ME.'.conf.php')) {
			require '/etc/'.ME.'.conf.php';
			if (isset($exempt) && is_array($exempt)) $GLOBALS['EXEMPT'] = $exempt;
			if (isset($ttl) && is_array($ttl)) $GLOBALS['TTL'] = $ttl;
			if (isset($permanent) && is_array($permanent)) $GLOBALS['PERMANENT'] = $permanent;
		}
		$new = array();
		foreach (file(BLOCKLIST, FILE_IGNORE_NEW_LINES) as $line)
			$new[substr($line, 0, $pos = strpos($line, ' '))] = substr($line, $pos + 1);
		foreach ($GLOBALS['ips'] as $ip => $stamp) if (! array_key_exists($ip, $new)) ip_unblock($GLOBALS['ips'], $ip);
		$GLOBALS['ips'] = $new;
		$GLOBALS['permranges'] = blocklist_load_permanent();
	}
}

function lock_exclusive($file, $wait=60) {
	$timeout = time() + $wait;
	$lock = fopen($file, 'c+');
	while (! flock($lock, LOCK_EX | LOCK_NB)) {
		sleep(2);
		if (time() >= $timeout) {
			fclose($lock);
			error('Error: Could not obtain exclusive lock on '.$file, LOG_MAIL|LOG_INFO);
			return FALSE;
		}
	}
	return $lock;
}

function unlock($lock) {
	flock($lock, LOCK_UN);
	fclose($lock);
	return TRUE;
}

function daemonize($stdin='/dev/null', $stdout='/dev/null', $stderr='/dev/null') {
	// Daemonizes running process, returns PID file lock. Forks, becomes session leader, forks again,
	// closes open STD handles so there's no zombie, opens replacements in global namespace.
	if (! ($lock = lock_pid())) {
		error('Error: '.ME.' could not obtain lock.');
		return FALSE; // Open lock file
	}
	if (pcntl_fork()) exit(); // Fork. If we get a PID, exit.  If we get 0, we're the child, continue
	if (posix_setsid() === -1) { // Dissociate from controlling terminal, become session leader
		error('Error: '.ME.' could not setsid.');
		return FALSE;
	}
	usleep(100000); // sleep 1/10th of a second

	// Fork again as session leader to be free of other processes. If pcntl_fork()
	// returns 0 we're the (grand)child, else we're the parent getting the PID of the (grand)child
	if ($childpid = pcntl_fork()) { // Write (grand)child's PID to lockfile
		ftruncate($lock, 0);
		rewind($lock);
		fwrite($lock, $childpid);
		exit; // If we got here, we're the parent, exit.
	}
	// If we get here, we're the (grand)child, finally independent.
	usleep(100000); // sleep 1/10th of a second

	// http://andytson.com/blog/2010/05/daemonising-a-php-cli-script-on-a-posix-system/
	// As we are a daemon, close standard file descriptors. When a standard file descriptor is closed,
	// it can be replaced. Create new standard file descriptors in case anything tries to use them.
	// Variable names are not important, but do not re-order the fopens. https://github.com/php/php-src/issues/11399
	fclose(STDIN); fclose(STDOUT); fclose(STDERR);
	$GLOBALS['STDIN'] = fopen($stdin, 'r'); // set fd0
	$GLOBALS['STDOUT'] = fopen($stdout, 'w'); // set fd1
	if ($stdout == $stderr) $GLOBALS['STDERR'] = fopen('php://stdout', 'w'); // hack to duplicate fd1 to fd2
	else $GLOBALS['STDERR'] = fopen($stderr, 'w'); // set fd2 or set fd2 to fd1

	// Ignore some signals we don't care about
	pcntl_signal(SIGTSTP, SIG_IGN);
	pcntl_signal(SIGTTOU, SIG_IGN);
	pcntl_signal(SIGTTIN, SIG_IGN);

	return $lock;
}

function lock_pid($lockpath=FALSE) {
	if (! $lockpath) $lockpath = LOCKFILE;
	$lock = fopen($lockpath, 'c+');
	if (! flock($lock, LOCK_EX | LOCK_NB)) return FALSE;
	ftruncate($lock, 0);
	rewind($lock);
	fwrite($lock, getmypid());
	return $lock;
}

function signal_handler($signo, $siginfo) {
	// $siginfo=array('signo' => 15, 'errno' => 0, 'code' => 0);
	switch ($signo) {
		case SIGINT:
		case SIGTERM:
			// handle shutdown tasks
			exit; // calls shutdown()
			break;
		case SIGHUP:
			// handle reload tasks
			reload();
			break;
		case SIGUSR1:
			// handle restart tasks
			restart();
			break;
	}
}

function signal_send($signal, $local=FALSE) {
	if ((! file_exists(LOCKFILE)) && (! $local)) {
		fwrite(STDERR, 'Warning: '.ME.' does not appear to be running. Cannot signal.'."\n");
		return FALSE;
	}
	$pid = (int) trim(file_get_contents(LOCKFILE));
	if ((! $pid) && (! $local)) {
		fwrite(STDERR, 'Warning: '.ME.' does not appear to be running.'."\n");
		return FALSE;
	}
	if ($pid && (! posix_kill($pid, $signal))) return FALSE;
	return TRUE;
}

function mail_report($from, $to, $subject, $content, $file='', $fromname='', $toname='') {
	// e-mails $content, returns Message-ID or FALSE. Meant to be self-contained
	$fromname = ($fromname) ? '"'.$fromname.'" ' : '';
	$toname = ($toname) ? '"'.$toname.'" ' : '';
	$sf = ini_get('sendmail_from');
	ini_set('sendmail_from', $from);
	$token = FALSE; // Token does not need to be secure
	if (function_exists('random_bytes')) $token = @random_bytes(8); // use builtin
	elseif (function_exists('openssl_random_pseudo_bytes')) $token = @openssl_random_pseudo_bytes(8); // use extension
	elseif (is_executable('/usr/bin/openssl')) { // shell out to command
		$handle = popen('/usr/bin/openssl rand 8', 'r');
		$token = stream_get_contents($handle);
		pclose($handle);
	}
	elseif ($token == '') // give up on 'secure' token; shuffle some characters around, it's good enough for a Message-ID
		$token = str_shuffle(substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, 8));
	$messageid = sprintf('%s.%s@%s', @base_convert(microtime(), 10, 36), @base_convert(bin2hex($token), 16, 36), php_uname('n'));
	$headers  = 'From: '.$fromname.' <'.$from.'>'."\r\n";
	$headers .= 'X-Mailer: '.((isset($GLOBALS['argv'])) ? basename($GLOBALS['argv'][0]) : basename($_SERVER['SCRIPT_NAME'])).' .v'.VERSION."\r\n";
	$headers .= 'Message-ID: <'.$messageid.'>'."\r\n";
	$message = "\r\n";
	if (is_array($content)) $content = implode("\n", $content);
	if ($content) $message .= $content."\r\n";
	if ($file && file_exists($file)) {
		if ($content) $message .= "\r\n";
		if ($handle = fopen($file, 'r')) {
			while (($line = fgets($handle)) !== FALSE) $message .= trim($line)."\r\n";
			fclose($handle);
		}
	}
	if (strtolower(substr(PHP_OS, 0, 6)) == 'win') $options = ''; else $options = '-f'.$from;
	$mailed = mail($to, $subject, $message, $headers, $options);
	ini_set('sendmail_from', $sf);
	return ($mailed) ? $messageid : FALSE;
}

function logfile($msg) {
	if (defined('LOGFILE') && LOGFILE) {
		$stamp = date('D M d H:i:s Y ');
		$msg = trim($msg);
		if ((! is_dir(dirname(LOGFILE))) && is_writable(dirname(LOGFILE))) mkdir(dirname(LOGFILE), 0700, TRUE);
		$omask = decoct(umask(0077));
		file_put_contents(LOGFILE, $stamp.$msg."\n", FILE_APPEND|LOCK_EX);
		umask($omask);
	}
}

function output($msg, $prio=LOG_INFO) {
	// Handle normal output: $prio is for syslog
	// Uses: DEBUG, STAMP, SYSLOG, $GLOBALS['MSG']
	// Attempts to output $msg on STDOUT, appends to filename LOGFILE.
	// Sends message to syslog if $prio !== FALSE .  LOG_NOTICE=normal but significant condition,
	// LOG_INFO=informational message,  LOG_DEBUG=debug-level message	$GLOBALS['MSG'] .= $msg."\n";
	// also: LOG_CONS (log to system console), LOG_MAIL, LOG_AUTH
	$stamp = date('D M d H:i:s Y ');
	if (! array_key_exists('MSG', $GLOBALS)) $GLOBALS['MSG'] = '';
	if (! array_key_exists('OUT', $GLOBALS)) $GLOBALS['OUT'] = '';
	if (! array_key_exists('ERR', $GLOBALS)) $GLOBALS['ERR'] = '';
	if ($msg === TRUE) return $GLOBALS['MSG'];
	$msg = trim($msg);
	$GLOBALS['MSG'] .= ((defined('STAMP') && STAMP) ? $stamp : '').$msg."\n";
	$GLOBALS['OUT'] .= ((defined('STAMP') && STAMP) ? $stamp : '').$msg."\n";
	if (defined('LOGFILE') && LOGFILE) {
		if ((! is_dir(dirname(LOGFILE))) && is_writable(dirname(LOGFILE))) mkdir(dirname(LOGFILE), 0700, TRUE);
		$omask = decoct(umask(0077));
		file_put_contents(LOGFILE, $stamp.$msg."\n", FILE_APPEND|LOCK_EX);
		umask($omask);
	}
	if (! array_key_exists('STDOUT', $GLOBALS)) echo $msg."\n";
	elseif (array_key_exists('STDOUT', $GLOBALS) && is_resource($GLOBALS['STDOUT'])) @fwrite($GLOBALS['STDOUT'], $msg."\n");
	if ((! defined('SYSLOG')) || (defined('SYSLOG') && SYSLOG)) syslog($prio, $msg);
}

function error($msg, $prio=LOG_INFO, $facility=LOG_USER) {
	// Handle error output: $prio is for syslog
	// Uses: DEBUG, STAMP, SYSLOG, $GLOBALS['MSG']
	// Attempts to output $msg on STDERR and syslog
	// LOG_EMERG=system is unusable,  LOG_ALERT=action must be taken immediately,  LOG_CRIT=critical conditions,
	// LOG_ERR=error conditions,  LOG_WARNING=warning conditions
	// also: LOG_CONS (log to system console), LOG_MAIL, LOG_AUTH
	$stamp = date('D M d H:i:s Y ');
	if (! array_key_exists('MSG', $GLOBALS)) $GLOBALS['MSG'] = '';
	if (! array_key_exists('OUT', $GLOBALS)) $GLOBALS['OUT'] = '';
	if (! array_key_exists('ERR', $GLOBALS)) $GLOBALS['ERR'] = '';
	$GLOBALS['ERROR'] = TRUE;
	$msg = trim($msg);
	$GLOBALS['MSG'] .= ((defined('STAMP') && STAMP) ? $stamp : '').$msg."\n";
	$GLOBALS['ERR'] .= ((defined('STAMP') && STAMP) ? $stamp : '').$msg."\n";
	if (defined('ALLERRORS') && ALLERRORS) {
		if ((! is_dir(dirname(ALLERRORS))) && (! get_uid(FALSE, TRUE))) mkdir(dirname(ALLERRORS), 0777, TRUE);
		if (! file_exists(ALLERRORS)) {
			// Mask so files are created 0027 -rw-r----- 0227 -r--r---- 0277 -r-------- 0007 -rw-rw---- 0000 -rw-rw-rw-
			$omask = decoct(umask(0000));
			touch(ALLERRORS);
			chmod(ALLERRORS, 0666);
			umask($omask);
		}
		file_put_contents(ALLERRORS, $stamp.basename(MEPATH).' ['.getmypid().']: '.$msg."\n", FILE_APPEND|LOCK_EX);
	}
	if (defined('LOGFILE') && LOGFILE) {
		if ((! is_dir(dirname(LOGFILE))) && is_writable(dirname(LOGFILE))) mkdir(dirname(LOGFILE), 0700, TRUE);
		$omask = decoct(umask(0077));
		file_put_contents(LOGFILE, $stamp.$msg."\n", FILE_APPEND|LOCK_EX);
		umask($omask);
	}
	if (defined('STDERR') && is_resource(STDERR)) @fwrite(STDERR, $msg."\n");
	elseif (array_key_exists('STDERR', $GLOBALS) && is_resource($GLOBALS['STDERR'])) @fwrite($GLOBALS['STDERR'], $msg."\n");
	if ((! defined('SYSLOG')) || SYSLOG) syslog($prio, $msg);
}

function hostname($fqdn=FALSE) {
	$hostname = php_uname('n');
	return ($fqdn) ? $hostname : substr($hostname, 0, strpos($hostname, '.'));
}

function restore_standard_timezone_policy(&$timezone=FALSE) {
	// Being explicitly told what the timezone is, is not a "guess" to be ignored.
	// Make PHP work correctly by again following decades long conventions.
	// * Use the explicitly provided timezone data *
	// 1. If application chooses a timezone, use that.
	// 2. Else, if the user's TZ if set, this takes priority.
	// 3. Else, if user has not set their TZ, fall back to the system's time zone.
	// 4. Else, if cannot find system timezone, fall back to UTC
	if (! $timezone) {
		$notset = TRUE;
		$timezone = 'UTC';
		$TZ = getenv('TZ');
		if ($TZ !== FALSE) {
			if (in_array($TZ, DateTimeZone::listIdentifiers())) {
				$notset = FALSE;
				$timezone = $TZ;
			}
			else {
				$error = 'Error: Invalid timezone: '.$TZ;
				if (function_exists('error')) error($error);
				else @fwrite(STDERR, $error."\n");
			}
		}
		if (! stristr(PHP_OS_FAMILY, 'windows')) {
			if ($notset && (file_exists('/etc/timezone'))) {
				// Debian / Ubuntu
				$data = file_get_contents('/etc/timezone');
				if ($data) {
					$notset = FALSE;
					$timezone = trim($data);
				}
			}
			if ($notset && file_exists('/etc/sysconfig/clock')) {
				// RHEL / CentOS
				$data = parse_ini_file('/etc/sysconfig/clock');
				if (! empty($data['ZONE'])) {
					$notset = FALSE;
					$timezone = $data['ZONE'];
				}
			}
			if ($notset && is_link('/etc/localtime')) {
				// Mac OSX (and older Linuxes)
				// /etc/localtime is a symlink to the timezone in /usr/share/zoneinfo or /var/db/timezone/zoneinfo
				$filename = readlink('/etc/localtime');
				if (strpos($filename, '/var/db/timezone/zoneinfo/') === 0) $timezone = substr($filename, 26);
				if (strpos($filename, '/usr/share/zoneinfo/') === 0) $timezone = substr($filename, 20);
			}
		}
		else { // Running under Windows
			$tz = exec('tzutil.exe /g', $out, $err);
			if (! $err) $timezone = intltz_get_id_for_windows_id($tz);
		}
	}
	else {
		if (! in_array($timezone, DateTimeZone::listIdentifiers())) {
			$error = 'Error: Invalid timezone: '.$timezone;
			if (function_exists('error')) error($error);
			else @fwrite(STDERR, $error."\n");
			$timezone = 'UTC';
		}
	}
	return (date_default_timezone_set($timezone)) ? $timezone : FALSE;
}

function formatstr($str, $cols=FALSE) {
	if (defined('COLUMNS') && (! $cols)) $cols = COLUMNS;
	return ($cols) ? wordwrap($str, $cols) : $str;
}

function terminal_init(&$rows=FALSE) {
	//	'tput cols'   tput used to, but no longer returns correct value when invoked by PHP exec()
	//	'resize'      works, not commonly installed, needs to parse: COLUMNS=167;\nLINES=48;\nexport COLUMNS LINES;\n
	// 'stty -a'     works, needs to parse: speed 38400 baud; rows 49; columns 167; line = 0;
	if (defined('COLUMNS')) return COLUMNS;
	$rows = FALSE; $cols = FALSE;
	$out = ''; $return = 0;
	exec('stty -a 2>/dev/null', $out, $return);
	if ($return == 0) {
		$out = strtolower(implode("\n", $out));
		if (FALSE !== preg_match_all("/rows.([0-9]+);.columns.([0-9]+);/", $out, $matches)) {
			$rows = $matches[1][0];
			$cols = $matches[2][0];
		}
	}
	if ($cols == FALSE) {
		$cols = exec('tput cols 2>/dev/null', $out, $return);
		if ($rows) $rows = exec('tput lines 2>/dev/null', $out, $return);
	}
	if (! $cols) $cols = 80;
	if (! defined('COLUMNS')) define('COLUMNS', $cols);
	if ($rows && (! defined('ROWS'))) define('ROWS', $rows);
	return $cols;
}

function help($stderr=FALSE) {
	terminal_init();
	$out = ($stderr === FALSE) ? STDOUT : STDERR;	$str = ME.' v. '.VERSION;
	$str .= ' Temporarily or permanently blocks or unblocks IP addresses given on command-line, or in --run and '
		.'--daemonize modes, monitors the mail server logfile to block assholes trying to guess SASL passwords.  '
		.'Additionally can monitor SMTP server failed session emails for specific error messages.  '
		.'Can also permanently block IPs of Abuse-As-A-Service and address verification services by domain names, '
		.'in the configuration file.  Whitelisting by IP or DNS name can be added to the configuration file.'
		."\n\n".'The default action is to block the given IP addresses.'."\n"
		."\n".'Config file is /etc/'.ME.'.php.conf'."\n";
	@fwrite($out, formatstr($str."\n", COLUMNS));
	@fwrite($out, formatstr('Usage: '.ME.' [options] [IP IP ...]'."\n", COLUMNS));
	@fwrite($out, formatstr("\n".'Options:'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-h|--help] (show this help, exit)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-v|--version] (show version number, exit)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-r|--run] (runs '.ME.' in monitor mode in foreground, use with systemd)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-D|--daemonize] (runs '.ME.' in monitor mode as a daemon)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-R|--restart] (restarts already running '.ME.')'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-L|--reload] (reloads certain config settings in '.ME.')'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-S|--stop] (shuts down already running '.ME.')'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-W|--show] (shows blocklist with human-readable timestamps, exit)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-b|--both] (unblocks from both permanent and temporary when given with --unblock)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-u|--unblock IP IP ...] (unblocks specified IPs previously blocked by '.ME.', exit)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-U|--unblockall] (unblocks all IPs blocked by '.ME.', exit)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-s|--stats] (shows the '.ME.' TTL and number of IPs blocked, exit)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-t|--type] (Type of IPs blocked)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-n|--name] (Name of IPs blocked)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-e|--expire] (unblock records whose TTL has expired, exit)'."\n", COLUMNS));
	@fwrite($out, formatstr('  [-p|--permanent] (makes blocking permanent)'."\n", COLUMNS));
	@fwrite($out, formatstr("\n".'When reloading, only the $ttl, $exempt, and $permanent settings can be '
		.'reloaded.  You must do a restart to change any other settings.'."\n", COLUMNS));
	@fwrite($out, formatstr("\n".'See the manpage '.ME.'(1) for more information.'."\n", COLUMNS));
}
