mailbox->isConfigured()) { return 0; } // The scheduled run records its own outcome, so the "last checked" line // in the console means the last time the mailbox was actually reached — // not the last time somebody pressed a button. A mailbox that stopped // answering at three in the morning is the one worth seeing, and nobody // presses a button at three in the morning. $check = $this->mailbox->check(); $this->status->record($check['ok'], $check['message'], $check['unseen']); if (! $check['ok']) { return 0; } $taken = 0; foreach ($this->mailbox->fetch($limit) as $mail) { $stored = $this->store($mail); if ($stored) { $taken++; } // Flagged only once the row is safely written — including for a // duplicate, which IS safely written, just earlier. Flagging first // and failing after would lose the message with nothing to show // for it, and there is no second copy anywhere. $this->mailbox->markSeen($mail->uid); } return $taken; } private function store(FetchedMail $mail): bool { $customer = Customer::query()->where('email', $mail->fromEmail)->first(); try { InboundMail::create([ 'customer_id' => $customer?->id, 'support_request_id' => $customer === null ? null : $this->openRequestFor($customer)?->id, 'message_id' => $mail->messageId, 'from_email' => $mail->fromEmail, 'from_name' => $mail->fromName, 'subject' => $mail->subject, 'body' => $mail->body, 'attachments' => $mail->attachments === [] ? null : $mail->attachments, 'received_at' => $mail->receivedAt, ]); } catch (UniqueConstraintViolationException) { // Already here. The fetch runs every few minutes and a flag that // did not stick on the server costs one re-read — which is exactly // what this catch turns into nothing. return false; } catch (\Throwable $e) { Log::error('Could not store an inbound mail', ['exception' => $e]); return false; } return true; } /** * The question this is probably an answer to. * * The customer's most recent request that is still open. A guess, and a * shallow one on purpose — it only pre-fills a link an operator can undo, * and threading by References/In-Reply-To would need the whole outgoing * side to carry stable ids first. */ private function openRequestFor(Customer $customer): ?SupportRequest { return SupportRequest::query() ->where('customer_id', $customer->id) ->whereNotIn('status', SupportRequest::CLOSED) ->latest('id') ->first(); } }