*/ 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, ] ); $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)) { $stripped = strip_tags($bodyHtml); $stripped = preg_replace('/\s+/', ' ', $stripped); 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); /** @var Collection $collection */ $collection = (new EmailBody)->getCollection(); $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(), ]); } }