Files
global-autoresponder/scripts/global_autoresponder_worker.php
T
2026-06-02 20:51:57 +02:00

172 lines
5.9 KiB
PHP
Executable File

#!/usr/local/bin/php
<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/exec/lib/Settings.php';
require_once dirname(__DIR__) . '/exec/lib/MessageClassifier.php';
require_once dirname(__DIR__) . '/exec/lib/RepeatState.php';
require_once dirname(__DIR__) . '/exec/lib/MailSender.php';
require_once dirname(__DIR__) . '/exec/lib/MailboxDirectory.php';
require_once dirname(__DIR__) . '/exec/lib/RuleRepository.php';
final class GlobalAutoresponderWorker
{
/** @param callable(string,string,string,string):bool $send */
public function __construct(
private Settings $settings,
private RuleRepository $rules,
private MailboxDirectory $mailboxes,
private RepeatState $repeat,
private $send,
private ?int $now = null
) {
}
/** @param array<string,string> $env */
public function handle(array $env, string $message): int
{
if ($this->settings->backendMode() !== 'exim') {
return 0;
}
$sender = self::firstEnv($env, ['SENDER_ADDRESS', 'sender_address', 'SENDER', 'sender']);
$recipient = self::recipientFromEnv($env);
if ($sender === '' || $recipient === '' || !self::isEmail($sender) || !self::isEmail($recipient)) {
return 0;
}
$headers = self::parseHeaders($message);
if (MessageClassifier::shouldSuppress($headers, $sender)) {
return 0;
}
$resolved = $this->mailboxes->resolveRecipient($recipient);
if ($resolved === null) {
return 0;
}
$sent = 0;
$owner = $resolved['owner'];
$recipient = $resolved['email'];
foreach ($this->rules->all($owner) as $rule) {
if (!$this->isActiveRule($rule)) {
continue;
}
if (!$this->ruleIncludesRecipient($rule, $recipient)) {
continue;
}
$ruleId = (string)($rule['id'] ?? '');
if ($ruleId === '') {
continue;
}
$repeatMinutes = $this->settings->replyEveryMessage() ? 0 : $this->settings->repeatMinutes();
if (!$this->repeat->shouldSend($owner, $ruleId, $recipient, $sender, $repeatMinutes)) {
continue;
}
$ok = (bool)call_user_func(
$this->send,
$recipient,
strtolower($sender),
(string)$rule['subject'],
(string)$rule['body']
);
if ($ok) {
$this->repeat->record($owner, $ruleId, $recipient, $sender);
$sent++;
}
}
return $sent;
}
/** @param array<string,mixed> $rule */
private function isActiveRule(array $rule): bool
{
if (empty($rule['enabled'])) {
return false;
}
$now = $this->now ?? time();
return (int)($rule['start_ts'] ?? 0) <= $now && (int)($rule['end_ts'] ?? 0) >= $now;
}
/** @param array<string,mixed> $rule */
private function ruleIncludesRecipient(array $rule, string $recipient): bool
{
if (!array_key_exists('dynamic_scope', $rule) || !empty($rule['dynamic_scope'])) {
return true;
}
$snapshot = $rule['mailbox_snapshot'] ?? [];
if (!is_array($snapshot)) {
return false;
}
foreach ($snapshot as $mailbox) {
if (is_array($mailbox) && strtolower((string)($mailbox['email'] ?? '')) === strtolower($recipient)) {
return true;
}
}
return false;
}
/** @param array<string,string> $env @param string[] $keys */
private static function firstEnv(array $env, array $keys): string
{
foreach ($keys as $key) {
$value = trim((string)($env[$key] ?? ''));
if ($value !== '') {
return strtolower($value);
}
}
return '';
}
/** @param array<string,string> $env */
private static function recipientFromEnv(array $env): string
{
$direct = self::firstEnv($env, ['LOCAL_PART_DOMAIN', 'RECIPIENT', 'ORIGINAL_RECIPIENT', 'local_part_domain']);
if ($direct !== '') {
return $direct;
}
$local = self::firstEnv($env, ['LOCAL_PART', 'local_part']);
$domain = self::firstEnv($env, ['DOMAIN', 'domain']);
return ($local !== '' && $domain !== '') ? $local . '@' . $domain : '';
}
private static function isEmail(string $value): bool
{
return (bool)preg_match('/^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+$/', $value);
}
/** @return array<string,string> */
public static function parseHeaders(string $message): array
{
$headers = [];
$current = '';
foreach (preg_split('/\r\n|\n|\r/', $message) ?: [] as $line) {
if ($line === '') {
break;
}
if ($current !== '' && preg_match('/^[ \t]/', $line)) {
$headers[$current] .= ' ' . trim($line);
continue;
}
if (!str_contains($line, ':')) {
continue;
}
[$key, $value] = explode(':', $line, 2);
$current = trim($key);
if ($current !== '') {
$headers[$current] = trim($value);
}
}
return $headers;
}
}
if (PHP_SAPI === 'cli' && realpath((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) {
$settings = Settings::load(Settings::EXTERNAL_SETTINGS);
$sender = new MailSender();
$worker = new GlobalAutoresponderWorker(
$settings,
new RuleRepository(),
new MailboxDirectory(),
new RepeatState(),
static fn (string $from, string $to, string $subject, string $body): bool => $sender->send('/usr/sbin/sendmail', $from, $to, $subject, $body)
);
$worker->handle(getenv(), stream_get_contents(STDIN) ?: '');
}