Implement IMAP email-to-ticket gateway
This commit is contained in:
1 parent
b7e5884457
commit
6f25869345
33 files changed
+5619
-5
No files matched your search
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/validation.php';
|
||||
|
||||
use Box\Mod\Support\Entity\Helpdesk;
|
||||
use Box\Mod\Support\Entity\SupportTicket;
|
||||
use Box\Mod\Support\Entity\SupportTicketMessage;
|
||||
|
||||
$ticket = new SupportTicket();
|
||||
(new ReflectionProperty($ticket, 'id'))->setValue($ticket, 4);
|
||||
$ticket->setClientId(7);
|
||||
$ticket->setStatus('on_hold');
|
||||
$helpdesk = new Helpdesk();
|
||||
$helpdesk->setCanReopen(true);
|
||||
$ticket->setSupportHelpdesk($helpdesk);
|
||||
$support = new class($ticket) extends \Box\Mod\Support\Service {
|
||||
public function __construct(private SupportTicket $ticket) {}
|
||||
public function setDi(\Pimple\Container $di): void { $this->di = $di; }
|
||||
public function getTicketById(int $id): SupportTicket
|
||||
{
|
||||
if ($id !== 4) {
|
||||
throw new \FOSSBilling\InformationException('Ticket not found');
|
||||
}
|
||||
return $this->ticket;
|
||||
}
|
||||
};
|
||||
$em = new class {
|
||||
public array $messages = [];
|
||||
public function persist($message): void
|
||||
{
|
||||
if (!$message instanceof SupportTicketMessage) {
|
||||
throw new RuntimeException('Unexpected entity');
|
||||
}
|
||||
(new ReflectionProperty($message, 'id'))->setValue($message, count($this->messages) + 10);
|
||||
$this->messages[] = $message;
|
||||
}
|
||||
public function flush(): void {}
|
||||
};
|
||||
$di = new \Pimple\Container();
|
||||
$lookup = new class {
|
||||
public array $ids = [7];
|
||||
public function fetchFirstColumn($sql, $params) { return $this->ids; }
|
||||
};
|
||||
$di['dbal'] = $lookup;
|
||||
$di['mailgateway_notification_cooldown'] = new class {
|
||||
public function run($sender, $queue): bool { return $queue() !== false; }
|
||||
};
|
||||
$di['db'] = new class($client) {
|
||||
public function __construct(public $client) {}
|
||||
public function getExistingModelById($type, $id) { return $this->client; }
|
||||
};
|
||||
$di['em'] = $em;
|
||||
$di['request'] = new \Symfony\Component\HttpFoundation\Request();
|
||||
$di['events_manager'] = new class {
|
||||
public array $events = [];
|
||||
public function fire($data): void { $this->events[] = $data; }
|
||||
};
|
||||
$di['logger'] = new class {
|
||||
public function info(...$args): void {}
|
||||
public function warning(...$args): void {}
|
||||
};
|
||||
$email = new class {
|
||||
public array $notifications = [];
|
||||
public bool $fail = false;
|
||||
public function sendTemplate($data): void
|
||||
{
|
||||
if ($this->fail) {
|
||||
throw new RuntimeException('Simulated mail failure');
|
||||
}
|
||||
$this->notifications[] = $data;
|
||||
}
|
||||
};
|
||||
$di['mod_service'] = $di->protect(fn ($module) => $module === 'email' ? $email : $support);
|
||||
$support->setDi($di);
|
||||
$service->setDi($di);
|
||||
$reply = ['ticket_id' => '4', 'sender_email' => 'customer@example.com', 'content' => ' Customer reply '];
|
||||
$result = $service->reply($reply);
|
||||
if ($result !== ['ticket_id' => 4, 'message_id' => 10]
|
||||
|| $ticket->getStatus() !== 'open' || $em->messages[0]->getClientId() !== 7
|
||||
|| $em->messages[0]->getAdminId() !== null || $em->messages[0]->getContent() !== 'Customer reply') {
|
||||
throw new RuntimeException('Customer reply attribution or status incorrect');
|
||||
}
|
||||
|
||||
foreach ([[], [8], [7, 8]] as $ids) {
|
||||
$lookup->ids = $ids;
|
||||
try {
|
||||
$service->reply($reply);
|
||||
throw new RuntimeException('Wrong or ambiguous sender accepted');
|
||||
} catch (\FOSSBilling\InformationException) {}
|
||||
}
|
||||
$lookup->ids = [7];
|
||||
foreach ([0, -1, '4x', [], true, null, 4.5, 999] as $id) {
|
||||
try {
|
||||
$service->reply(array_replace($reply, ['ticket_id' => $id]));
|
||||
throw new RuntimeException('Invalid or missing ticket accepted');
|
||||
} catch (\FOSSBilling\InformationException) {}
|
||||
}
|
||||
$ticket->setStatus('closed');
|
||||
$helpdesk->setCanReopen(false);
|
||||
try {
|
||||
$service->reply($reply);
|
||||
throw new RuntimeException('Reopen restriction bypassed');
|
||||
} catch (\FOSSBilling\InformationException) {}
|
||||
$helpdesk->setCanReopen(true);
|
||||
$service->reply($reply);
|
||||
|
||||
// A guest ticket retains its guest owner even if that address later registers.
|
||||
$ticket->setClientId(null);
|
||||
$ticket->setAccessHash('private-hash');
|
||||
$ticket->setAuthorEmail('guest@example.com');
|
||||
$ticket->setStatus('on_hold');
|
||||
$reply['sender_email'] = 'GUEST@example.com';
|
||||
$service->reply($reply);
|
||||
$message = $em->messages[2];
|
||||
if ($message->getClientId() !== null || $message->getAdminId() !== null || $ticket->getStatus() !== 'open') {
|
||||
throw new RuntimeException('Guest reply attribution or status incorrect');
|
||||
}
|
||||
$reply['sender_email'] = 'other@example.com';
|
||||
try {
|
||||
$service->reply($reply);
|
||||
throw new RuntimeException('Wrong guest accepted');
|
||||
} catch (\FOSSBilling\InformationException) {}
|
||||
if (count($em->messages) !== 3) {
|
||||
throw new RuntimeException('Rejected reply created a message');
|
||||
}
|
||||
if ($email->notifications !== []) {
|
||||
throw new RuntimeException('Direct reply processing attempted a notification');
|
||||
}
|
||||
$service->notifySenderMismatch('other@example.com', 4);
|
||||
foreach ($email->notifications as $notification) {
|
||||
if ($notification !== [
|
||||
'to' => $notification['sender_email'],
|
||||
'code' => 'mailgateway_sender_mismatch',
|
||||
'sender_email' => $notification['sender_email'],
|
||||
'ticket_id' => 4,
|
||||
]) {
|
||||
throw new RuntimeException('Mismatch notification contains incorrect or private ticket data');
|
||||
}
|
||||
}
|
||||
$email->fail = true;
|
||||
$service->notifySenderMismatch('other@example.com', 4);
|
||||
try {
|
||||
$service->reply($reply);
|
||||
throw new RuntimeException('Mail failure allowed a mismatched reply');
|
||||
} catch (\FOSSBilling\InformationException) {}
|
||||
if (count($em->messages) !== 3) {
|
||||
throw new RuntimeException('Mail failure created a message');
|
||||
}
|
||||
echo "Passed customer and guest reply attribution, sender ownership, ambiguity, ticket validation, and reopen policy using Support ticketReply.\n";
|
||||
echo "Passed mismatch notification recipient, private data exclusion, and rejection despite mail failure.\n";
|
||||
Reference in New Issue
Block a user