query('thema', 'general'); return view('contact', [ 'topic' => in_array($topic, self::TOPICS, true) ? $topic : 'general', 'sent' => $request->session()->get('contact.sent', false), ]); } public function send(Request $request): RedirectResponse { $data = $request->validate([ 'company' => ['nullable', 'string', 'max:120'], 'name' => ['required', 'string', 'max:120'], 'email' => ['required', 'email:rfc', 'max:190'], 'phone' => ['nullable', 'string', 'max:60'], 'message' => ['required', 'string', 'min:10', 'max:4000'], 'topic' => ['nullable', 'string', 'in:'.implode(',', self::TOPICS)], // Honigtopf: ein Feld, das im Browser niemand sieht und ein // Formularroboter trotzdem ausfüllt. `filled` statt einer eigenen // Regel, damit die Meldung, die ein Mensch nie zu sehen bekommt, // auch keine eigene Übersetzung braucht. 'website' => ['nullable', 'prohibited'], ], [], [ 'name' => 'Name', 'email' => 'E-Mail-Adresse', 'message' => 'Nachricht', ]); $enquiry = [ 'company' => $data['company'] ?? null, 'name' => $data['name'], 'email' => $data['email'], 'phone' => $data['phone'] ?? null, 'message' => $data['message'], 'topic' => $data['topic'] ?? 'general', ]; // ZUERST festhalten, dann erst benachrichtigen. // // Die Mail war anfangs die einzige Ablage, und das ging in dem Moment // schief, in dem der Versand scheiterte: der Besucher las „Ihre Anfrage // ist da", und die Anfrage lag in `failed_jobs`, wo niemand nachsieht. // Genau so ist es hier beim ersten Versuch passiert — das Postfach // support@ hatte kein eigenes SMTP-Konto, und der Server wies den // Absender ab. // // Der Posteingang der Konsole ist ohnehin die richtige Ablage: er // existiert für „was Kunden uns geschrieben haben", zeigt Nicht- // Zugeordnetes zuerst und nennt in seinem eigenen Kopfkommentar die // neue Anfrage von einer unbekannten Adresse als den Fall, der nicht // untergehen darf. Eine Website-Anfrage IST dieser Fall. $this->record($enquiry); // Die Mail ist ab hier eine Benachrichtigung, kein Ablageort. Scheitert // sie, steht die Anfrage trotzdem in der Konsole. Mail::send(new ContactRequestMail($enquiry)); // Zurück auf dieselbe Seite, mit einer Fahne in der Sitzung statt einer // Kennung in der Adresse: ein Neuladen zeigt danach wieder das leere // Formular und nicht dauerhaft eine Bestätigung für etwas, das längst // erledigt ist. return redirect() ->route('contact') ->with('contact.sent', true); } /** * Die Anfrage in den Posteingang der Konsole legen. * * @param array{company: ?string, name: string, email: string, phone: ?string, message: string, topic: ?string} $enquiry */ private function record(array $enquiry): void { InboundMail::create([ // Dieselbe Zuordnungsregel wie beim Abholen echter Mail // (IngestInboundMail): schreibt ein bestehender Kunde über das // Formular, hängt die Anfrage an ihm. Sonst bleibt sie ohne // Zuordnung — und steht damit im Posteingang ganz oben. 'customer_id' => Customer::query()->where('email', $enquiry['email'])->value('id'), // Synthetisch, weil eine Formularanfrage keine echte Message-ID hat // und die Spalte eindeutig sein muss. Das Präfix macht später // erkennbar, dass dieser Eintrag nicht aus dem Postfach kam. 'message_id' => 'web-'.Str::uuid().'@'.(parse_url((string) config('app.url'), PHP_URL_HOST) ?: 'clupilot'), 'from_email' => $enquiry['email'], 'from_name' => $enquiry['company'] ? $enquiry['name'].' · '.$enquiry['company'] : $enquiry['name'], 'subject' => ContactRequestMail::subjectFor($enquiry), 'body' => $this->bodyFor($enquiry), 'received_at' => now(), ]); } /** * Was der Posteingang als Nachricht zeigt. * * Die Telefonnummer gehört mit hinein: `inbound_mails` hat kein Feld dafür, * und eine Nummer, die jemand extra angegeben hat, im Formular zu verlieren * hieße, ihn nicht anrufen zu können. * * @param array{phone: ?string, message: string} $enquiry */ private function bodyFor(array $enquiry): string { return $enquiry['phone'] ? $enquiry['message']."\n\n—\nTelefon: ".$enquiry['phone'] : $enquiry['message']; } }