A comprehensive help desk and ticket management system for Laravel applications with email integration.
- PHP 8.1+
- Laravel 10, 11, or 12
composer require jeffersongoncalves/laravel-help-deskThe package uses Laravel's auto-discovery, so the service provider and facade are registered automatically.
php artisan vendor:publish --tag=help-desk-configphp artisan vendor:publish --tag=help-desk-migrationsphp artisan migratephp artisan vendor:publish --tag=help-desk-translationsThe configuration file is located at config/help-desk.php. Key options:
return [
// Models used by the help desk
'models' => [
'user' => \App\Models\User::class, // Model that creates tickets
'operator' => \App\Models\User::class, // Model that manages tickets
],
// Ticket settings
'ticket' => [
'reference_prefix' => 'HD', // Ticket reference format: HD-00001
'default_status' => 'open',
'default_priority' => 'medium',
'attachment_disk' => 'local', // Storage disk for attachments
'auto_close_days' => null, // Auto-close resolved tickets (null = disabled)
'allow_reopen' => true,
],
// Email integration
'email' => [
'enabled' => true,
'inbound' => [
'driver' => null, // 'imap', 'mailgun', 'sendgrid', or 'resend'
],
],
// Notification settings
'notifications' => [
'channels' => ['mail'],
'queue' => 'default',
],
];For regular users (ticket creators):
use JeffersonGoncalves\HelpDesk\Concerns\HasTickets;
class User extends Authenticatable
{
use HasTickets;
}For operators/agents (ticket managers):
use JeffersonGoncalves\HelpDesk\Concerns\IsOperator;
class User extends Authenticatable
{
use IsOperator; // Includes HasTickets
}use JeffersonGoncalves\HelpDesk\Facades\HelpDesk;
$department = HelpDesk::createDepartment([
'name' => 'Technical Support',
'slug' => 'technical-support',
'email' => 'support@example.com',
'is_active' => true,
]);HelpDesk::addOperator($department, $user, 'operator'); // 'operator', 'manager', or 'admin'use JeffersonGoncalves\HelpDesk\Facades\HelpDesk;
$ticket = HelpDesk::createTicket([
'title' => 'Cannot access my account',
'description' => 'I get an error when trying to log in...',
'department_id' => $department->id,
'priority' => 'high',
], $user);
// $ticket->reference_number => "HD-00001"
// $ticket->uuid => "550e8400-e29b-41d4-a716-446655440000"// Find tickets
$ticket = HelpDesk::findTicketByReference('HD-00001');
$ticket = HelpDesk::findTicketByUuid('550e8400-...');
// Assign to operator
HelpDesk::assignTicket($ticket, $operator);
HelpDesk::unassignTicket($ticket);
// Change status
use JeffersonGoncalves\HelpDesk\Enums\TicketStatus;
HelpDesk::changeStatus($ticket, TicketStatus::InProgress);
HelpDesk::closeTicket($ticket);
HelpDesk::reopenTicket($ticket);
// Update ticket
HelpDesk::updateTicket($ticket, [
'priority' => 'urgent',
'category_id' => $category->id,
]);
// Delete ticket (soft delete)
HelpDesk::deleteTicket($ticket);// Add a public reply
$comment = HelpDesk::addComment($ticket, $user, 'Thank you for contacting us.');
// Add an internal note (not visible to end user)
$note = HelpDesk::addNote($ticket, $operator, 'Escalating to senior engineer.');
// Add comment with attachments
$comment = HelpDesk::addComment($ticket, $user, 'See attached screenshot.', [
'attachments' => [$uploadedFile],
]);HelpDesk::addWatcher($ticket, $anotherUser);
HelpDesk::removeWatcher($ticket, $anotherUser);use JeffersonGoncalves\HelpDesk\Models\Ticket;
use JeffersonGoncalves\HelpDesk\Enums\TicketStatus;
use JeffersonGoncalves\HelpDesk\Enums\TicketPriority;
// Open tickets
$open = Ticket::open()->get();
// Closed tickets
$closed = Ticket::closed()->get();
// By status
$inProgress = Ticket::byStatus(TicketStatus::InProgress)->get();
// By priority
$urgent = Ticket::byPriority(TicketPriority::Urgent)->get();
// Overdue tickets
$overdue = Ticket::overdue()->get();
// Unassigned tickets
$unassigned = Ticket::unassigned()->get();
// User's tickets (via trait)
$user->helpDeskTickets;
// Operator's assigned tickets (via trait)
$operator->helpDeskAssignedTickets;use JeffersonGoncalves\HelpDesk\Models\CannedResponse;
CannedResponse::create([
'title' => 'Greeting',
'body' => 'Thank you for contacting our support team...',
'department_id' => $department->id,
'is_active' => true,
]);
// Get canned responses for a department
$responses = CannedResponse::active()
->forDepartment($department->id)
->ordered()
->get();use JeffersonGoncalves\HelpDesk\Models\Category;
$category = Category::create([
'department_id' => $department->id,
'name' => 'Billing',
'slug' => 'billing',
'is_active' => true,
]);
// Subcategories
$sub = Category::create([
'department_id' => $department->id,
'parent_id' => $category->id,
'name' => 'Refunds',
'slug' => 'refunds',
]);| Status | Description |
|---|---|
open |
New ticket, awaiting response |
pending |
Awaiting user response |
in_progress |
Being worked on by an operator |
on_hold |
Temporarily on hold |
resolved |
Issue has been resolved |
closed |
Ticket is closed |
Status transitions are validated automatically. For example, a closed ticket can only transition to open (reopen).
| Priority | Numeric Value |
|---|---|
low |
1 |
medium |
2 |
high |
3 |
urgent |
4 |
The package dispatches events that you can listen to in your application:
| Event | Description |
|---|---|
TicketCreated |
A new ticket was created |
TicketUpdated |
A ticket was updated |
TicketStatusChanged |
Ticket status changed |
TicketPriorityChanged |
Ticket priority changed |
TicketAssigned |
Ticket was assigned to an operator |
TicketClosed |
Ticket was closed |
TicketReopened |
Ticket was reopened |
TicketDeleted |
Ticket was deleted |
CommentAdded |
A comment was added to a ticket |
AttachmentAdded |
An attachment was added |
AttachmentRemoved |
An attachment was removed |
InboundEmailReceived |
An inbound email was received |
InboundEmailProcessed |
An inbound email was processed |
If you want to handle events yourself:
// config/help-desk.php
'register_default_listeners' => false,Notifications are sent automatically when events occur (configurable via notifications.notify_on). Email threading is supported via Message-ID, In-Reply-To, and References headers.
The package supports receiving emails via 5 drivers:
HELPDESK_INBOUND_DRIVER=imap
HELPDESK_IMAP_HOST=imap.example.com
HELPDESK_IMAP_PORT=993
HELPDESK_IMAP_ENCRYPTION=ssl
HELPDESK_IMAP_USERNAME=support@example.com
HELPDESK_IMAP_PASSWORD=your-password
HELPDESK_IMAP_FOLDER=INBOXRequires the webklex/php-imap package:
composer require webklex/php-imapSchedule the polling command in your app/Console/Kernel.php or routes/console.php:
$schedule->command('help-desk:poll-imap')->everyFiveMinutes();HELPDESK_INBOUND_DRIVER=mailgun
HELPDESK_MAILGUN_SIGNING_KEY=your-signing-keyConfigure your Mailgun route to forward to:
POST https://your-app.com/help-desk/webhooks/mailgun
HELPDESK_INBOUND_DRIVER=sendgrid
HELPDESK_SENDGRID_WEBHOOK_USERNAME=your-username
HELPDESK_SENDGRID_WEBHOOK_PASSWORD=your-passwordConfigure your SendGrid Inbound Parse to forward to:
POST https://your-app.com/help-desk/webhooks/sendgrid
HELPDESK_INBOUND_DRIVER=resend
HELPDESK_RESEND_API_KEY=re_your-api-key
HELPDESK_RESEND_WEBHOOK_SECRET=whsec_your-webhook-secretConfigure your Resend receiving domain webhook to forward to:
POST https://your-app.com/help-desk/webhooks/resend
Select the email.received event type in your Resend webhook configuration.
HELPDESK_INBOUND_DRIVER=postmark
HELPDESK_POSTMARK_WEBHOOK_USERNAME=your-username
HELPDESK_POSTMARK_WEBHOOK_PASSWORD=your-passwordIn your Postmark server, go to the Inbound Message Stream settings and set the webhook URL to:
POST https://your-username:your-password@your-app.com/help-desk/webhooks/postmark
Postmark sends the full email content (body, headers, attachments) directly in the webhook payload. The package also uses Postmark's StrippedTextReply field for cleaner reply parsing.
You can configure multiple email channels, each mapped to a department:
use JeffersonGoncalves\HelpDesk\Models\EmailChannel;
EmailChannel::create([
'department_id' => $department->id,
'name' => 'Support Inbox',
'driver' => 'mailgun',
'email_address' => 'support@example.com',
'settings' => [], // Driver-specific settings (encrypted)
'is_active' => true,
]);When an inbound email is received, the package resolves it to an existing ticket using:
In-Reply-ToheaderReferencesheader- Subject line reference number (e.g.,
HD-00001)
If no match is found, a new ticket is created.
# Poll IMAP mailboxes for new emails
php artisan help-desk:poll-imap
# Clean old processed inbound emails
php artisan help-desk:clean-emails --days=30
# Auto-close stale tickets
php artisan help-desk:close-stale --days=14 --status=resolved
# Dry run (see what would be closed)
php artisan help-desk:close-stale --days=14 --dry-runFor more control, you can inject the service classes directly:
use JeffersonGoncalves\HelpDesk\Services\TicketService;
use JeffersonGoncalves\HelpDesk\Services\CommentService;
use JeffersonGoncalves\HelpDesk\Services\DepartmentService;
use JeffersonGoncalves\HelpDesk\Services\AttachmentService;
class MyController
{
public function __construct(
private TicketService $tickets,
private CommentService $comments,
) {}
public function store(Request $request)
{
$ticket = $this->tickets->create([
'title' => $request->title,
'description' => $request->description,
'department_id' => $request->department_id,
], $request->user());
return $ticket;
}
}The package ships with English and Brazilian Portuguese translations. To customize:
php artisan vendor:publish --tag=help-desk-translationsThis publishes translation files to lang/vendor/help-desk/. You can modify them or add new locales.
// Using translations in your code
__('help-desk::tickets.messages.created') // "Ticket created successfully."
__('help-desk::statuses.open') // "Open"
__('help-desk::priorities.urgent') // "Urgent"composer testcomposer analysecomposer formatThe MIT License (MIT). Please see License File for more information.
