151 lines
5.2 KiB
PHP
Executable File
151 lines
5.2 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;
|
|
}
|
|
$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,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) ?: '');
|
|
}
|