PHPackages                             rasmuscnielsen/laravel-support-tickets - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. rasmuscnielsen/laravel-support-tickets

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

rasmuscnielsen/laravel-support-tickets
======================================

Email-based support tickets for Laravel. IMAP inbox sync, threading, drafts, lifecycle events and AI-powered similarity search.

v1.0.0(today)00MITPHPPHP ^8.3CI passing

Since Jul 23Pushed todayCompare

[ Source](https://github.com/rasmuscnielsen/laravel-support-tickets)[ Packagist](https://packagist.org/packages/rasmuscnielsen/laravel-support-tickets)[ RSS](/packages/rasmuscnielsen-laravel-support-tickets/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (12)Versions (2)Used By (0)

[![Laravel Support Tickets](art/logo.svg)](art/logo.svg)

[![Latest Stable Version](https://camo.githubusercontent.com/570588bdb212121b1e6e62dde9562361fef906ee70cb036fabfdd787cc4ef56c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7261736d7573636e69656c73656e2f6c61726176656c2d737570706f72742d7469636b657473)](https://packagist.org/packages/rasmuscnielsen/laravel-support-tickets)[![Total Downloads](https://camo.githubusercontent.com/ac03a987ac7a349bf715e3c742d4229ef4c59a3d51cf1200279ee391d8baf906/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7261736d7573636e69656c73656e2f6c61726176656c2d737570706f72742d7469636b657473)](https://packagist.org/packages/rasmuscnielsen/laravel-support-tickets)[![License](https://camo.githubusercontent.com/48c0a043c23bd1046968dd81d1c30f442c9e006a30da2d886f2bf8d625695c00/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f7261736d7573636e69656c73656e2f6c61726176656c2d737570706f72742d7469636b657473)](https://packagist.org/packages/rasmuscnielsen/laravel-support-tickets)

Laravel Support Tickets
=======================

[](#laravel-support-tickets)

**Email-based support tickets for Laravel: IMAP inbox sync, threading, drafts, lifecycle events, and AI-powered similarity search.**

This package turns one or more shared mailboxes into a full support ticket backend. It polls your inboxes over IMAP, parses raw MIME into clean reply/signature/quote segments, threads messages into tickets, and sends your replies back out through Laravel's mailer with correct `In-Reply-To`/`References` headers so conversations stay threaded in your customers' mail clients.

It is a **headless** package: models, actions, workflows, jobs, commands, and events, but no UI. Build your agent-facing interface on top with Filament, Inertia, Livewire, or anything else.

- **Inbox sync**: poll IMAP folders incrementally, store raw MIME, dedupe by provider UID and `Message-ID`. Or skip IMAP and [feed the ingest workflow](#bring-your-own-ingest) from inbound-mail webhooks.
- **Parsing**: extract reply text, signature, and quoted trail from both plain text and HTML emails (via `zbateson/mail-mime-parser` and `willdurand/email-reply-parser`), with UTF-8 scrubbing for hostile input.
- **Threading**: resolve inbound mail to existing tickets through `In-Reply-To`, `References`, and subject/participant heuristics with a confidence score.
- **Drafts &amp; replies**: compose rich HTML drafts with quoted parent content and per-agent signatures, then send immediately or on a schedule, idempotently.
- **Lifecycle**: assign, resolve, close, reopen, spam, prioritize. Every mutation records an audit `TicketEvent`.
- **AI similarity**: embed ticket conversations via the [Laravel AI SDK](https://github.com/laravel/ai) and find similar historical tickets with pgvector distance search.

Requirements
------------

[](#requirements)

- PHP 8.3+, Laravel 12+
- Any database Laravel supports (MySQL, PostgreSQL, MariaDB, SQLite), with one exception: the [AI similarity search](#ai-similarity-search) feature needs a **PostgreSQL** connection with the [pgvector](https://github.com/pgvector/pgvector) extension. Everything else works normally; only similarity search is off the table. The ticket tables can also live on a dedicated Postgres connection (see `database_connection` below) while your app's primary database stays MySQL.

Installation
------------

[](#installation)

```
composer require rasmuscnielsen/laravel-support-tickets
```

Publish the config and run the migrations:

```
php artisan vendor:publish --tag=support-tickets-config
php artisan migrate
```

Schedule the commands that keep mail flowing:

```
use Illuminate\Support\Facades\Schedule;

Schedule::command('support-tickets:inbox:sync')->everyFiveMinutes();
Schedule::command('support-tickets:drafts:send-scheduled')->everyMinute();
Schedule::command('support-tickets:tickets:close-resolved')->hourly();
```

How it works
------------

[](#how-it-works)

 ```
flowchart LR
    customer([Customer]) -- "email" --> mailbox[IMAP mailbox]
    mailbox -- "inbox:sync" --> sync

    subgraph pkg [laravel-support-tickets]
        sync["Sync, parse& thread"] --> db[("Tickets &messages")]
        send["Send threadedreply"]
    end

    subgraph app [Your app]
        ui["Agent UI"]
    end

    db  ui
    ui -- "SendDraft" --> send
    send -- "reply" --> customer
```

      Loading 1. **Sync**: `support-tickets:inbox:sync` dispatches a `SyncInboxMessages` job per enabled inbox. The job pulls new messages from the configured IMAP folder (incrementally, using a per-inbox sync cursor) and hands each one to the `ReceiveInboundMessage` workflow.
2. **Receive**: the workflow stores the message (raw MIME included), parses the body into reply/signature/quote segments, resolves it to an existing ticket via threading or opens a new one, and fires `InboundMessageReceived`.
3. **Reply**: your UI reads tickets and messages straight from the models, and composes drafts with `UpsertTicketDraft`. Dispatching `SendDraft` runs the `SendDraftReply` workflow: the draft is rendered with signature and quoted trail, sent through Laravel's mailer with proper threading headers, and marked as sent. `OutboundMessageSent` fires.
4. **React**: configured listeners respond to `InboundMessageReceived` and `OutboundMessageSent`. By default the ticket conversation is re-vectorized when vectorization is enabled, and [AI-drafted replies](#ai-drafted-replies) hook in the same way.

Data model
----------

[](#data-model)

All tables are prefixed `support_` and all models live in `Rasmuscnielsen\LaravelSupportTickets\Models`:

ModelTablePurpose`Inbox``support_inboxes`A synced mailbox, linked to a config profile by name.`Ticket``support_tickets`One conversation: status, priority, requester, assignee, tags, SLA timestamps.`Message``support_messages`One email or note: direction, type, status, addressing, threading metadata, raw MIME.`MessageContent``support_message_contents`Parsed body segments: `text`, `html`, `signature_text`, `quoted_text`, `full_text`, `full_html`.`Attachment``support_attachments`Stored files with disk/path, MIME type, checksum, inline flag.`TicketEvent``support_ticket_events`Audit trail: every lifecycle change with actor and properties.`Embedding``support_embeddings`Embeddings of ticket conversations (pgvector on Postgres).`Ticket.requester`, `Ticket.assignee`, `Ticket.context`, and `Message.createdBy` are morph relations. Point them at whatever `User`/`Customer`/`Order` models your app has.

Email is the first-class channel, but the schema is deliberately not email-shaped and can host other channels too. More on that under [More channels](#more-channels).

Useful entry points:

```
use Rasmuscnielsen\LaravelSupportTickets\Enums\TicketStatus;
use Rasmuscnielsen\LaravelSupportTickets\Models\Ticket;

Ticket::open()->get();                         // New, Open, or Resolved
$ticket->messages()->delivered()->get();       // what the customer saw
$ticket->drafts;                               // unsent drafts
$ticket->latestMessage->content->text;         // parsed reply text
$ticket->events;                               // audit trail
```

Configuration
-------------

[](#configuration)

The published `config/support-tickets.php` at a glance:

```
return [
    // Connection for all package tables (null = your default connection).
    'database_connection' => env('SUPPORT_TICKETS_DB_CONNECTION'),

    // Enum for Message::$type; swap in your own to add channels.
    'enums' => ['message_type' => MessageType::class],

    // Named bundles of sender identity, inbound IMAP settings and outbound mailer.
    'profiles' => [
        'default' => [
            'sender' => [
                'address' => env('SUPPORT_TICKETS_SENDER_ADDRESS'),
                'name' => env('SUPPORT_TICKETS_SENDER_NAME'),
            ],
            'inbound' => [
                'driver' => 'imap',
                'imap_mailbox' => env('SUPPORT_TICKETS_INBOUND_IMAP_MAILBOX', 'default'),
                'folders' => [
                    'all' => env('SUPPORT_TICKETS_INBOUND_FOLDER_ALL', 'INBOX'),
                    'drafts' => env('SUPPORT_TICKETS_INBOUND_FOLDER_DRAFTS', 'Drafts'),
                ],
            ],
            'outbound' => [
                'driver' => 'mail',
                'mailer' => env('SUPPORT_TICKETS_OUTBOUND_MAILER'),
            ],
        ],
    ],

    // Storage and limits for message attachments.
    'attachments' => [
        'disk' => env('SUPPORT_TICKETS_ATTACHMENTS_DISK', 'local'),
        'max_bytes' => env('SUPPORT_TICKETS_ATTACHMENTS_MAX_BYTES', 5 * 1024 * 1024),
        'allowed_mime_types' => [/* images */],
        'inbound_allowed_mime_types' => [/* images, pdf, office documents, ... */],
        'outbound_allowed_mime_types' => [/* images, pdf, office documents, ... */],
        'outbound_max_files' => env('SUPPORT_TICKETS_OUTBOUND_ATTACHMENTS_MAX_FILES', 5),
    ],

    // Composition of outgoing replies.
    'replies' => [
        'signature_resolver' => NullTicketReplySignatureResolver::class,
        'quote_attribution_date_format' => env('SUPPORT_TICKETS_QUOTE_ATTRIBUTION_DATE_FORMAT', 'd/m/Y H.i'),
    ],

    // Close tickets that stay resolved, via the scheduled command.
    'auto_close_resolved_tickets' => [
        'enabled' => env('SUPPORT_TICKETS_AUTO_CLOSE_RESOLVED_TICKETS_ENABLED', true),
        'after_days' => env('SUPPORT_TICKETS_AUTO_CLOSE_RESOLVED_TICKETS_AFTER_DAYS', 7),
    ],

    // Listeners wired to the package's lifecycle events at boot.
    'listeners' => [
        InboundMessageReceived::class => [VectorizeLifecycleMessage::class],
        OutboundMessageSent::class => [VectorizeLifecycleMessage::class],
    ],

    // Ticket conversation embeddings for similarity search (Postgres + pgvector).
    'vectorization' => [
        'enabled' => env('SUPPORT_TICKETS_VECTORIZATION_ENABLED', false),
        'provider' => env('SUPPORT_TICKETS_VECTORIZATION_PROVIDER'),
        'model' => env('SUPPORT_TICKETS_VECTORIZATION_MODEL'),
        'redactor' => BasicContentRedactor::class,
    ],
];
```

Most keys speak for themselves. The ones with more going on each have a section below: [Signatures](#signatures), [Events](#events) for the listener map, and [AI similarity search](#ai-similarity-search) for the redactor and vectorization.

### Profiles and inboxes

[](#profiles-and-inboxes)

The mail-facing configuration is organized around **profiles**: named bundles of sender identity, inbound IMAP settings, and outbound mailer. Each `Inbox` row points at a profile via its `profile` column, so several inboxes can share one profile or each have their own.

`imap_mailbox` refers to a mailbox defined in the `imap.php` config from [directorytree/imapengine-laravel](https://github.com/DirectoryTree/ImapEngine-Laravel), which this package uses for IMAP access. Configure your host and credentials there.

Create an inbox and syncing starts on the next scheduler tick:

```
use Rasmuscnielsen\LaravelSupportTickets\Models\Inbox;

Inbox::create([
    'name' => 'Support',
    'profile' => 'default',
    'enabled' => true,
]);
```

Replying to tickets
-------------------

[](#replying-to-tickets)

Compose or update a draft with `UpsertTicketDraft`. It builds the outgoing content (your text, the resolved agent signature, and a quoted trail of the parent message) in both HTML and plain text:

```
use Rasmuscnielsen\LaravelSupportTickets\Actions\UpsertTicketDraft;
use Rasmuscnielsen\LaravelSupportTickets\Jobs\SendDraft;

$draft = app(UpsertTicketDraft::class)->handle(
    ticket: $ticket,
    parentMessage: $ticket->latestMessage,
    text: 'Hi Jane, your order shipped this morning.',
    html: 'Hi Jane, your order shipped this morning.',
);

SendDraft::dispatch($draft);
```

Sending is **idempotent**: the draft gets a stable `Message-ID` before the mailer is invoked, so a retried job can't double-send. Failures mark the draft `FailedSending` and rethrow.

To schedule a reply instead of sending immediately, mark the draft `Scheduled` with a send time (pass `status` and `scheduled_send_at` via the `attributes` argument, or set them on the draft). The `support-tickets:drafts:send-scheduled` command queues due drafts, and holds any draft whose ticket has received a newer customer message in the meantime, so you never fire a stale scheduled reply.

### Signatures

[](#signatures)

Bind your own resolver to give each agent a personal signature on outgoing replies:

```
// config/support-tickets.php
'replies' => [
    'signature_resolver' => App\Support\AgentSignatureResolver::class,
    // ...
],
```

```
use Rasmuscnielsen\LaravelSupportTickets\Contracts\ResolvesTicketReplySignature;
use Rasmuscnielsen\LaravelSupportTickets\Models\Message;
use Rasmuscnielsen\LaravelSupportTickets\ValueObjects\TicketReplySignature;

class AgentSignatureResolver implements ResolvesTicketReplySignature
{
    public function resolve(Message $message): ?TicketReplySignature
    {
        return new TicketReplySignature(
            text: "Regards\n{$message->createdBy?->name}",
            html: "Regards{$message->createdBy?->name}",
        );
    }
}
```

The default `NullTicketReplySignatureResolver` adds no signature.

### Internal notes

[](#internal-notes)

Notes live on the ticket as messages of type `Note`. They are never emailed, but they are part of the conversation and the audit trail:

```
use Rasmuscnielsen\LaravelSupportTickets\Actions\CreateInternalNote;

app(CreateInternalNote::class)->handle($ticket, 'Customer called. Refund approved.', actor: $user);
```

`UpdateInternalNote` and `DeleteInternalNote` complete the set.

Ticket lifecycle
----------------

[](#ticket-lifecycle)

Each lifecycle mutation is a single-purpose action that updates the ticket, stamps the relevant timestamp, and records a `TicketEvent` with the acting model:

```
use Rasmuscnielsen\LaravelSupportTickets\Actions\AssignTicket;
use Rasmuscnielsen\LaravelSupportTickets\Actions\ChangeTicketPriority;
use Rasmuscnielsen\LaravelSupportTickets\Actions\CloseTicket;
use Rasmuscnielsen\LaravelSupportTickets\Actions\MarkTicketAsSpam;
use Rasmuscnielsen\LaravelSupportTickets\Actions\ReopenTicket;
use Rasmuscnielsen\LaravelSupportTickets\Actions\ResolveTicket;
use Rasmuscnielsen\LaravelSupportTickets\Enums\TicketPriority;

app(AssignTicket::class)->handle($ticket, $agent, actor: $user);
app(ChangeTicketPriority::class)->handle($ticket, TicketPriority::Urgent, actor: $user);
app(ResolveTicket::class)->handle($ticket, actor: $user);
app(CloseTicket::class)->handle($ticket, actor: $user);
app(ReopenTicket::class)->handle($ticket, actor: $user);
app(MarkTicketAsSpam::class)->handle($ticket, actor: $user);
```

Statuses flow `New → Open → Resolved → Closed` (plus `Spam`). Resolved tickets are auto-closed by the scheduled command after `auto_close_resolved_tickets.after_days`. A new inbound customer message flips an open or resolved ticket back to `New`, while closed and spam tickets stay put.

AI similarity search
--------------------

[](#ai-similarity-search)

Enable vectorization to embed each ticket's conversation and search for similar historical tickets. Useful for suggesting past answers to agents, or to an AI drafting assistant:

```
'vectorization' => [
    'enabled' => env('SUPPORT_TICKETS_VECTORIZATION_ENABLED', false),
    'provider' => env('SUPPORT_TICKETS_VECTORIZATION_PROVIDER'),   // e.g. 'openai'
    'model' => env('SUPPORT_TICKETS_VECTORIZATION_MODEL'),         // e.g. 'text-embedding-3-small'
],
```

This feature requires the package tables to live on Postgres with pgvector, and it is the only part of the package that does. With vectorization disabled (the default), the vector code paths are never touched and any database driver works.

Vectorization is built on the [Laravel AI SDK](https://github.com/laravel/ai) (`laravel/ai`), so `provider` must be one of the providers configured in your app's `config/ai.php`, which is also where the API credentials live. The package itself holds no AI credentials; if embeddings work in your app via the SDK, they work here.

With vectorization on, the default `VectorizeLifecycleMessage` listener re-embeds the conversation whenever a message arrives or a reply is sent. Backfill older tickets with `support-tickets:tickets:vectorize-missing`.

Before content is embedded it passes through the bound `RedactsSupportTicketContent` implementation. The default `BasicContentRedactor` strips email addresses and phone numbers. Bind your own via `vectorization.redactor` for stricter PII handling.

Then:

```
use Rasmuscnielsen\LaravelSupportTickets\Actions\FindSimilarTickets;

$similar = app(FindSimilarTickets::class)->handle($ticket, limit: 5);

foreach ($similar as $embedding) {
    $embedding->ticket;      // the similar ticket
    $embedding->distance;    // vector distance (lower = more similar)
}
```

An optional closure lets you constrain the search (e.g. to resolved tickets only):

```
app(FindSimilarTickets::class)->handle($ticket, filter: fn ($query) => $query
    ->whereHas('ticket', fn ($q) => $q->where('status', TicketStatus::Closed)));
```

Events
------

[](#events)

Two package events are your integration points for notifications, auto-triage, AI draft generation, and the like:

```
Rasmuscnielsen\LaravelSupportTickets\Events\InboundMessageReceived  // ->message
Rasmuscnielsen\LaravelSupportTickets\Events\OutboundMessageSent     // ->message, ->draft
```

Register listeners the normal Laravel way, or through the package's `listeners` config map, which the service provider wires at boot:

```
'listeners' => [
    InboundMessageReceived::class => [
        VectorizeLifecycleMessage::class,
        App\Listeners\NotifySupportChannel::class,
    ],
],
```

Make it your own
----------------

[](#make-it-your-own)

The IMAP-to-mailer pipeline is one good setup, not the only one. Underneath, the package is a set of plain actions, workflows, and events, and each piece can be driven directly or replaced. A few patterns that fall out of that:

### Bring your own ingest

[](#bring-your-own-ingest)

IMAP polling is the batteries-included default, not a requirement. The sync job is just one caller of the `ReceiveInboundMessage` workflow, and everything downstream (threading, parsing, ticket state, events, vectorization) starts there. If your mail arrives some other way (a Mailgun/Postmark/SES inbound webhook, a Microsoft Graph subscription, another system's export), normalize it to an array and hand it to the workflow yourself:

```
use Rasmuscnielsen\LaravelSupportTickets\Workflows\ReceiveInboundMessage;

Route::post('/webhooks/inbound-mail', function (Request $request) {
    $message = app(ReceiveInboundMessage::class)->handle($inbox, [
        'subject' => $request->input('subject'),
        'mail_from' => [['address' => $request->input('sender'), 'name' => $request->input('from_name')]],
        'mail_to' => [['address' => 'support@example.com']],
        'mail_message_id' => $request->input('message_id'),
        'mail_in_reply_to_message_id' => $request->input('in_reply_to'),
        'mail_references' => $request->input('references', []),
        'text' => $request->input('body-plain'),
        'html' => $request->input('body-html'),
        'mail_raw_mime' => $request->input('body-mime'),   // optional but enables the richest parsing
        'mail_provider' => 'mailgun',
        'mail_provider_message_id' => $request->input('token'),
        'received_at' => now(),
    ]);

    return response()->noContent();
});
```

The workflow dedupes on `Message-ID` and provider message id, so webhook retries are safe. If you skip IMAP entirely, simply don't schedule `support-tickets:inbox:sync`.

### AI-drafted replies

[](#ai-drafted-replies)

Because drafts are first-class records and every inbound message fires an event, an AI copilot is a listener away: generate a suggested reply for each new customer message, store it as a draft, and let agents review, edit, and send from your UI. Nothing reaches the customer until a human (or your own automation) says so.

```
use Rasmuscnielsen\LaravelSupportTickets\Actions\ConvertPlainTextToEditorHtml;
use Rasmuscnielsen\LaravelSupportTickets\Actions\FindSimilarTickets;
use Rasmuscnielsen\LaravelSupportTickets\Actions\UpsertTicketDraft;
use Rasmuscnielsen\LaravelSupportTickets\Events\InboundMessageReceived;

class DraftReplyToInboundMessage
{
    public function handle(InboundMessageReceived $event): void
    {
        $ticket = $event->message->ticket;

        if (! $ticket || $ticket->drafts()->exists()) {
            return;
        }

        // Build context: the parsed conversation, plus e.g. similar resolved tickets.
        $conversation = $ticket->messages()->delivered()->with('content')->get();
        $similar = app(FindSimilarTickets::class)->handle($ticket, limit: 3);

        // ReplyAgent is your Laravel AI SDK agent, instructed on how to
        // write replies for your product.
        $reply = ReplyAgent::make()->prompt(json_encode([
            'subject' => $ticket->subject,
            'conversation' => $conversation->map(fn ($message) => [
                'direction' => $message->direction,
                'text' => $message->content?->text,
            ]),
            'similar_resolved_tickets' => $similar->map(fn ($embedding) => $embedding->content),
        ]))->text;

        app(UpsertTicketDraft::class)->handle(
            ticket: $ticket,
            parentMessage: $event->message,
            text: $reply,
            html: app(ConvertPlainTextToEditorHtml::class)->handle($reply),
            attributes: ['created_by_type' => 'system'],
        );
    }
}
```

Register it in the `listeners` config next to the vectorization listener, and pair it with [AI similarity search](#ai-similarity-search) so the model can lean on how similar tickets were resolved before.

### More channels

[](#more-channels)

The schema is deliberately not email-shaped. The `mail_*` addressing and threading columns are all nullable metadata rather than required fields, and content, attachments, and events hang off `Message` generically. `Message.type` is extensible by design: point `enums.message_type` at your own backed enum to add cases, and `$message->type` comes back as *your* enum. The only contract is that it includes the `Email` and `Note` values the package's own behavior keys off (assigning the package's `MessageType` cases keeps working too, since values are matched by backing value rather than enum class):

```
enum AppMessageType: string
{
    case Email = 'Email';
    case Note = 'Note';
    case Chat = 'Chat';
}
```

So storing chat messages on a ticket works today. What the package does *not* yet ship is the machinery for other channels: ingest, threading, and the reply path are email-specific. A new channel means bringing your own ingest and send path, with the data model ready to receive it.

Translations
------------

[](#translations)

Outgoing quote attributions ("On {date}, {sender} wrote:") ship in English and Danish. Publish and adjust, or add your locale:

```
php artisan vendor:publish --tag=support-tickets-lang
```

The mail text view is publishable too, via `--tag=support-tickets-views`.

Testing
-------

[](#testing)

The test suite runs against a real PostgreSQL database by default, so all similarity-search tests can execute against pgvector. Create the database, then:

```
composer test
```

Connection defaults live in `phpunit.xml.dist` (`127.0.0.1:5432`, user `postgres`, database `support_tickets_testing`). The suite also passes on MySQL (`DB_CONNECTION=mysql DB_PORT=3306 ...`), where the Postgres-only similarity tests are skipped.

License
-------

[](#license)

MIT. See [LICENSE.md](LICENSE.md).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/8465957?v=4)[Rasmus Nielsen](/maintainers/rasmuscnielsen)[@rasmuscnielsen](https://github.com/rasmuscnielsen)

---

Top Contributors

[![rasmuscnielsen](https://avatars.githubusercontent.com/u/8465957?v=4)](https://github.com/rasmuscnielsen "rasmuscnielsen (5 commits)")

---

Tags

laravelemailsupportimapticketshelpdesk

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/rasmuscnielsen-laravel-support-tickets/health.svg)

```
[![Health](https://phpackages.com/badges/rasmuscnielsen-laravel-support-tickets/health.svg)](https://phpackages.com/packages/rasmuscnielsen-laravel-support-tickets)
```

###  Alternatives

[unopim/unopim

UnoPim Laravel PIM

10.5k2.4k](/packages/unopim-unopim)[leantime/leantime

Open source project management system for non-project managers. Simple like Trello, powerful like Jira. Built with neurodiversity in mind.

10.2k4.0k](/packages/leantime-leantime)[code16/sharp

Laravel Content Management Framework

79164.7k9](/packages/code16-sharp)[directorytree/imapengine

A fully-featured IMAP library -- without the PHP extension

551284.6k10](/packages/directorytree-imapengine)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9421.6k64](/packages/open-dxp-opendxp)[synergitech/laravel-postal

This library integrates Postal with the standard Laravel mail framework.

38117.1k](/packages/synergitech-laravel-postal)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
