*/ public array $backoff = [5, 15, 30]; /** * Create a new job instance. * * @param array $payload The validated webhook payload. */ public function __construct( public array $payload, ) { $this->onQueue('emails'); $this->onConnection('redis'); } /** * Get the middleware the job should pass through. * * @return array */ public function middleware(): array { return [ (new WithoutOverlapping($this->payload['hash']))->dontRelease(), ]; } /** * Execute the job. */ public function handle(): void { $metadata = $this->payload['metadata']; $bodyText = $this->payload['bodyText'] ?? null; $bodyHtml = $this->payload['bodyHtml'] ?? null; $preview = $this->generatePreview($bodyText, $bodyHtml); $email = Email::updateOrCreate( ['unique_id_hash' => $this->payload['hash']], [ 'recipient_email' => $metadata['recipientEmail'], 'recipient_name' => $metadata['recipientName'] ?? '', 'sender_email' => $metadata['senderEmail'], 'sender_name' => $metadata['senderName'] ?? '', 'domain' => $metadata['domain'], 'subject' => $metadata['subject'] ?? '', 'preview' => $preview, 'attachments_json' => $metadata['attachments'] ?? [], 'attachment_size' => $metadata['attachmentSize'] ?? 0, 'received_at' => $metadata['received_at'], ] ); EmailBody::updateOrCreate( ['unique_id_hash' => $this->payload['hash']], [ 'body_text' => $bodyText, 'body_html' => $bodyHtml, ] ); // Track analytics for receiving email $mailbox = \App\Models\Mailbox::where('address', $email->recipient_email)->first(); TrackAnalytics::dispatch( eventType: 'email_received', mailboxHash: $mailbox?->mailbox_hash ?? 'unknown', domainHash: $mailbox?->domain_hash ?? 'unknown', metadata: [ 'email_id' => $email->id, 'sender' => $email->sender_email, 'recipient' => $email->recipient_email, 'attachment_count' => count($metadata['attachments'] ?? []), 'found_mailbox' => $mailbox !== null, ], ipAddress: '0.0.0.0', // Server-side event userAgent: 'MailOps/IncomingWorker' ); $this->ensureTtlIndex(); NewEmailReceived::dispatch($email); } /** * Generate an excerpt from the email body for the preview column. * * Prefers body_text. Falls back to body_html with tags stripped. */ private function generatePreview(?string $bodyText, ?string $bodyHtml): string { if (! empty($bodyText)) { return mb_substr(trim($bodyText), 0, 500); } if (! empty($bodyHtml)) { // Replace all HTML tags with spaces to prevent words from running together $html = preg_replace('/<[^>]*>/', ' ', $bodyHtml); // Decode HTML entities (e.g.  , &) $decoded = html_entity_decode($html ?? '', ENT_QUOTES | ENT_HTML5, 'UTF-8'); // Collapse multiple spaces into a single space $stripped = preg_replace('/\s+/', ' ', $decoded); return mb_substr(trim($stripped ?? ''), 0, 500); } return ''; } /** * Ensure the MongoDB TTL index exists on the `recent_email_bodies` collection. * * Uses a cache flag to avoid checking on every job execution. */ private function ensureTtlIndex(): void { Cache::rememberForever('mongodb_ttl_index_ensured', function () { $ttlSeconds = config('services.mailops.email_body_ttl_seconds', 259200); EmailBody::raw(function ($collection) use ($ttlSeconds) { /* @var \MongoDB\Collection $collection */ $collection->createIndex( ['created_at' => 1], ['expireAfterSeconds' => $ttlSeconds, 'name' => 'ttl_created_at'] ); }); return true; }); } /** * Handle a job failure. */ public function failed(?\Throwable $exception): void { Log::error('ProcessIncomingEmail failed', [ 'hash' => $this->payload['hash'] ?? 'unknown', 'error' => $exception?->getMessage(), ]); } }