Add email integration for ticket system and IMAP inbound support

Email integration: IMAP polling via webklex/php-imap (M365/Zoho/MIAB
compatible), TicketEmailProcessor with 3-strategy ticket matching
(In-Reply-To, References, subject line [EZSCALE-{id}]), signature and
quote stripping, scheduled command every 2min.

Outbound: 4 notification classes (TicketCreated, CustomerReply,
StaffReply, StatusChanged) with email threading headers via
withSymfonyMessage(). Staff replies set Reply-To: support@ezscale.cloud.

Frontend: ticket reference chips on all pages, "Via Email" badges on
email-originated replies. 12 new tests (163 total, 816 assertions).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-02-09 17:10:04 -05:00
parent a4cf7026bc
commit 015949a7f6
28 changed files with 1220 additions and 6 deletions

View File

@@ -173,6 +173,15 @@ function formatDate(dateStr: string): string {
</td>
<td class="text-body-2 font-weight-medium">
{{ ticket.subject }}
<VChip
v-if="ticket.ticket_reference"
size="x-small"
color="secondary"
variant="tonal"
class="ms-2"
>
{{ ticket.ticket_reference }}
</VChip>
</td>
<td>
<div class="d-flex flex-column">

View File

@@ -97,6 +97,14 @@ function getUserInitial(name: string): string {
<div>
<div class="d-flex align-center gap-2">
<span class="text-h4 font-weight-bold">Ticket #{{ ticket.id }}</span>
<VChip
v-if="ticket.ticket_reference"
size="small"
color="secondary"
variant="tonal"
>
{{ ticket.ticket_reference }}
</VChip>
<VChip
:color="resolveTicketStatusColor(ticket.status)"
size="small"
@@ -162,7 +170,7 @@ function getUserInitial(name: string): string {
</VAvatar>
<div>
<div class="text-body-1 font-weight-medium">
{{ reply.user?.name ?? 'Unknown' }}
{{ reply.user?.name ?? reply.from_email ?? 'Unknown' }}
<VChip
v-if="reply.is_staff_reply"
size="x-small"
@@ -171,6 +179,15 @@ function getUserInitial(name: string): string {
>
Staff
</VChip>
<VChip
v-if="reply.via_email"
size="x-small"
color="info"
variant="tonal"
class="ms-2"
>
Via Email
</VChip>
</div>
<div class="text-caption text-medium-emphasis">
{{ formatDateTime(reply.created_at) }}

View File

@@ -73,6 +73,15 @@ function formatStatus(status: string): string {
<tr v-for="ticket in tickets.data" :key="ticket.id">
<td class="text-body-2 font-weight-medium">
{{ ticket.subject }}
<VChip
v-if="ticket.ticket_reference"
size="x-small"
color="secondary"
variant="tonal"
class="ms-2"
>
{{ ticket.ticket_reference }}
</VChip>
</td>
<td>
<VChip

View File

@@ -76,6 +76,14 @@ function getUserInitial(name: string): string {
<div>
<div class="d-flex align-center ga-3 mb-2">
<span class="text-h5 font-weight-bold">{{ ticket.subject }}</span>
<VChip
v-if="ticket.ticket_reference"
size="small"
color="secondary"
variant="tonal"
>
{{ ticket.ticket_reference }}
</VChip>
<VChip
:color="resolveTicketStatusColor(ticket.status)"
size="small"
@@ -134,7 +142,7 @@ function getUserInitial(name: string): string {
</VAvatar>
<div>
<div class="text-body-1 font-weight-medium">
{{ reply.user?.name ?? 'Unknown' }}
{{ reply.user?.name ?? reply.from_email ?? 'Unknown' }}
<VChip
v-if="reply.is_staff_reply"
size="x-small"
@@ -143,6 +151,15 @@ function getUserInitial(name: string): string {
>
Staff
</VChip>
<VChip
v-if="reply.via_email"
size="x-small"
color="info"
variant="tonal"
class="ms-2"
>
Via Email
</VChip>
</div>
<div class="text-caption text-medium-emphasis">
{{ formatDateTime(reply.created_at) }}