$context * @param Closure(): void $send */ public function sendOnce( string $key, string $type, ?string $tenantCode, array $context, string $recipient, Closure $send, ): bool { $now = now(); EmailDelivery::query()->insertOrIgnore([ 'idempotency_key' => $key, 'email_type' => $type, 'tenant_code' => $tenantCode, 'status' => EmailDelivery::STATUS_PENDING, 'attempts' => 0, 'context' => json_encode($context, JSON_THROW_ON_ERROR), 'recipient_fingerprint' => $this->recipientFingerprint($recipient), 'created_at' => $now, 'updated_at' => $now, ]); $claimToken = (string) Str::uuid(); $leaseExpiresAt = $now->copy()->addSeconds( max(1, (int) config('mail.delivery_lease_seconds', 300)), ); $claimed = EmailDelivery::query() ->where('idempotency_key', $key) ->where(function ($query) use ($now): void { $query->whereIn('status', [ EmailDelivery::STATUS_PENDING, EmailDelivery::STATUS_FAILED, ])->orWhere(function ($query) use ($now): void { $query->where('status', EmailDelivery::STATUS_PROCESSING) ->where('lease_expires_at', '<=', $now); }); }) ->update([ 'status' => EmailDelivery::STATUS_PROCESSING, 'attempts' => new Expression('attempts + 1'), 'context' => json_encode($context, JSON_THROW_ON_ERROR), 'recipient_fingerprint' => $this->recipientFingerprint($recipient), 'claim_token' => $claimToken, 'claimed_at' => $now, 'lease_expires_at' => $leaseExpiresAt, 'failed_at' => null, 'last_error' => null, 'updated_at' => $now, ]) === 1; if (! $claimed) { return false; } try { $send(); EmailDelivery::query() ->where('idempotency_key', $key) ->where('claim_token', $claimToken) ->update([ 'status' => EmailDelivery::STATUS_SENT, 'claim_token' => null, 'lease_expires_at' => null, 'sent_at' => now(), 'updated_at' => now(), ]); } catch (Throwable $exception) { EmailDelivery::query() ->where('idempotency_key', $key) ->where('claim_token', $claimToken) ->update([ 'status' => EmailDelivery::STATUS_FAILED, 'claim_token' => null, 'lease_expires_at' => null, 'failed_at' => now(), 'last_error' => Str::limit($exception::class, 2000, ''), 'updated_at' => now(), ]); throw $exception; } return true; } private function recipientFingerprint(string $recipient): string { return hash_hmac( 'sha256', mb_strtolower(trim($recipient)), (string) config('app.key'), ); } }