executeStatement('CREATE TABLE mailgateway_notification (sender_key TEXT PRIMARY KEY, last_notified_at INTEGER NOT NULL)'); $sent = 0; $send = function () use (&$sent): bool { ++$sent; return true; }; $limiter = new \Box\Mod\Mailgateway\NotificationCooldown($connection); $newRun = new \Box\Mod\Mailgateway\NotificationCooldown($connection); if (!$limiter->run('Guest@example.com', $send) || $newRun->run('guest@example.com', $send) || $sent !== 1) { throw new RuntimeException('New polling run reset cooldown'); } $key = hash('sha256', 'guest@example.com'); $connection->update('mailgateway_notification', ['last_notified_at' => time() - 300], ['sender_key' => $key]); if ($newRun->run('guest@example.com', $send)) { throw new RuntimeException('Five-minute poll bypassed cooldown'); } $connection->update('mailgateway_notification', ['last_notified_at' => time() - 3601], ['sender_key' => $key]); if (!$newRun->run('guest@example.com', $send)) { throw new RuntimeException('Expired cooldown did not permit notification'); } if ($limiter->run('disabled@example.com', fn () => false) || !$limiter->run('disabled@example.com', $send)) { throw new RuntimeException('Disabled template consumed cooldown'); } $before = (int) $connection->fetchOne('SELECT COUNT(*) FROM test_messages'); try { $limiter->run('failure@example.com', function () use ($connection): void { $connection->insert('test_messages', ['ticket_id' => 41]); throw new RuntimeException('Queue failed'); }); throw new LogicException('Queue failure swallowed'); } catch (RuntimeException $error) { if ($error->getMessage() !== 'Queue failed') { throw $error; } } if ((int) $connection->fetchOne('SELECT COUNT(*) FROM test_messages') !== $before || !$limiter->run('failure@example.com', $send)) { throw new RuntimeException('Failed queue did not roll back'); } $connection->beginTransaction(); $limiter->run('transaction@example.com', $send); $connection->rollBack(); if (!$limiter->run('transaction@example.com', $send)) { throw new RuntimeException('Outer rollback retained cooldown'); } $connection->executeStatement('CREATE TABLE client (id INTEGER PRIMARY KEY, email TEXT)'); $di = new \Pimple\Container(); $di['dbal'] = $connection; $di['mailgateway_settings'] = new class { public function get(): array { return ['support_helpdesk_id' => null]; } }; $di['em'] = new class { public function flush(): void {} public function clear(): void {} }; $di['logger'] = new class { public function warning(...$args): void {} }; $mail = new class { public array $emails = []; public function sendTemplate($data): bool { $this->emails[] = $data; return true; } }; $ticket = new \Box\Mod\Support\Entity\SupportTicket(); (new ReflectionProperty($ticket, 'id'))->setValue($ticket, 50); $ticket->setAccessHash('guest-hash'); $ticket->setAuthorEmail('guest-owner@example.com'); $support = new class($di, $ticket) { public int $created = 0; public int $replies = 0; public bool $fail = false; public function __construct(public $di, public $ticket) {} public function getHelpdeskRepository() { return $this; } public function findBy($criteria, $order, $limit) { $desk = new \Box\Mod\Support\Entity\Helpdesk(); (new ReflectionProperty($desk, 'id'))->setValue($desk, 1); return [$desk]; } public function ticketCreateForGuest($data) { ++$this->created; foreach (['mod_support_ticket_open', 'mod_support_ticket_staff_reply'] as $code) { $this->di['mod_service']('email')->sendTemplate(['code' => $code, 'to' => $data['email']]); } if ($this->fail) { throw new RuntimeException('Support failed'); } return 'guest-hash'; } public function findOneByHash($hash) { return $this->ticket; } public function getTicketById($id) { return $this->ticket; } public function canBeReopened($ticket): bool { return true; } public function ticketReply($ticket, $identity, $content): int { return ++$this->replies; } }; $lookup = fn ($module, $sub = '') => $module === 'email' ? $mail : $support; $di['mod_service'] = $di->protect($lookup); $service = new \Box\Mod\Mailgateway\Service(); $service->setDi($di); $input = ['sender_email' => 'guest-owner@example.com', 'subject' => 'Test', 'content' => 'Test body']; $service->import($input); $service->import($input); if ($support->created !== 2 || count($mail->emails) !== 1 || $di['mod_service'] !== $lookup) { throw new RuntimeException('Guest notification cooldown or lookup restoration failed'); } $service->reply(['sender_email' => $input['sender_email'], 'ticket_id' => 50, 'content' => 'Human reply']); if ($support->replies !== 1) { throw new RuntimeException('Human reply blocked by cooldown'); } foreach ([1, 2] as $attempt) { try { $tracker->process(['mailbox' => 'support@example.com', 'message_id' => "", 'sender_email' => 'wrong@example.com', 'subject' => '[Ticket #50] Reply', 'content' => 'Mismatch'], $service); throw new RuntimeException('Mismatch accepted'); } catch (\FOSSBilling\InformationException) {} } if (count($mail->emails) !== 2 || $mail->emails[1]['code'] !== 'mailgateway_sender_mismatch') { throw new RuntimeException('Mismatch cooldown failed'); } $support->fail = true; try { $service->import($input); } catch (RuntimeException $error) { if ($error->getMessage() !== 'Support failed') { throw $error; } } if ($di['mod_service'] !== $lookup) { throw new RuntimeException('Failure left email interception active'); } echo "Passed persistent cooldown, five-minute polls, expiry, failed/disabled queues, nested rollback, guest acknowledgements, mismatch notifications, human replies, and service restoration.\n";