v1.2.0
- E-mail intake (IMAP), optional and off by default: clients can create a ticket or reply to an existing one just by sending/replying to an e-mail. Configure any number of mailboxes in the new Admin > Poczta page (SMTP + IMAP together, replacing the old "E-MAIL" tab), each routed to a specific subcategory or a whole category (new tickets.category_id column). Replies are matched to their ticket via the number/checksum already in every notification subject; autoresponders/bounces are detected and rejected; "restrict tickets to LDAP" is enforced for e-mail like the guest web form. Manual "Pobierz teraz" per-mailbox fetch button; dedicated storage/logs/imap-*.log regardless of the app's log level; mail-icon badges on e-mail-originated tickets/messages in the operator queue and ticket view. - Operator queue: "select all" checkbox in the table header for every currently visible ticket under the active filter/tab. - Fixed: scheduled commands (SLA breach check, automation rules, and now IMAP fetch) always sent notifications through .env's default mailer instead of the configured SMTP server, because AppServiceProvider's Settings override used to skip itself for any console command, not just migrate. - Fixed: visiting a ticket that no longer exists (deleted mid-session, or a stale background refresh) showed a raw 404 instead of redirecting back to the operator queue / client dashboard. - Docs: README/ARCHITECTURE/CLAUDE/install/wiki updated for all of the above, including the previously-missing host crontab entry for schedule:run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
65
src/app/Models/ImapMailbox.php
Normal file
65
src/app/Models/ImapMailbox.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* One inbound mailbox polled by `emails:fetch-imap` — an admin can configure
|
||||
* several (e.g. zgloszenia-it@ vs zgloszenia-delegacje@), each landing new
|
||||
* tickets in its own default subcategory. Unlike LDAP/SMTP/BookStack, this is
|
||||
* a list of N configs rather than a Settings singleton, so it's a real model
|
||||
* rather than key/value rows.
|
||||
*/
|
||||
#[Fillable([
|
||||
'name', 'enabled', 'host', 'port', 'encryption', 'validate_cert',
|
||||
'username', 'password', 'folder', 'processed_folder', 'rejected_folder',
|
||||
'default_subcategory_id', 'default_category_id', 'blocklist_senders', 'last_checked_at', 'last_error',
|
||||
])]
|
||||
class ImapMailbox extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => 'boolean',
|
||||
'validate_cert' => 'boolean',
|
||||
'password' => 'encrypted',
|
||||
'last_checked_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function defaultSubcategory(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Subcategory::class, 'default_subcategory_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Only meaningful when default_subcategory_id is null — a mailbox is
|
||||
* routed to either a specific subcategory or a whole category, never
|
||||
* both (enforced by the admin form's single combined selector).
|
||||
*/
|
||||
public function defaultCategory(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Category::class, 'default_category_id');
|
||||
}
|
||||
|
||||
public function blocklistedSenders(): array
|
||||
{
|
||||
return array_filter(array_map('trim', explode(',', (string) $this->blocklist_senders)));
|
||||
}
|
||||
|
||||
public function targetLabel(): string
|
||||
{
|
||||
if ($this->defaultSubcategory) {
|
||||
return $this->defaultSubcategory->category->name.' / '.$this->defaultSubcategory->name;
|
||||
}
|
||||
|
||||
if ($this->defaultCategory) {
|
||||
return 'Cała kategoria: '.$this->defaultCategory->name;
|
||||
}
|
||||
|
||||
return '—';
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
#[Fillable([
|
||||
'number', 'checksum', 'customer_id', 'email', 'name', 'subcategory_id', 'subject', 'body',
|
||||
'status_key', 'priority_key', 'team_id', 'assignee_id', 'custom_fields', 'api_client_id',
|
||||
'number', 'checksum', 'customer_id', 'email', 'name', 'subcategory_id', 'category_id', 'subject', 'body',
|
||||
'status_key', 'priority_key', 'team_id', 'assignee_id', 'custom_fields', 'api_client_id', 'source',
|
||||
'sla_notified_at', 'last_customer_activity_at', 'time_spent_seconds', 'timer_started_at',
|
||||
'created_at', 'updated_at', 'csat_rating', 'csat_comment', 'csat_rated_at',
|
||||
])]
|
||||
@@ -72,6 +72,16 @@ class Ticket extends Model
|
||||
return $this->belongsTo(Subcategory::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only ever set when there's no subcategory to derive a category from
|
||||
* (subcategory_id already implies one via Subcategory::category()) — see
|
||||
* categoryLabel() and the migration that introduced this column.
|
||||
*/
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
public function watchers(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'ticket_watchers');
|
||||
@@ -214,7 +224,7 @@ class Ticket extends Model
|
||||
|
||||
public function categoryLabel(): string
|
||||
{
|
||||
return $this->subcategory?->label() ?? '';
|
||||
return $this->subcategory?->label() ?? $this->category?->name ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
|
||||
|
||||
#[Fillable(['ticket_id', 'author_name', 'internal', 'body', 'edited', 'api_client_id', 'created_at', 'updated_at'])]
|
||||
#[Fillable(['ticket_id', 'author_name', 'internal', 'body', 'edited', 'api_client_id', 'source', 'created_at', 'updated_at'])]
|
||||
class TicketMessage extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
|
||||
Reference in New Issue
Block a user