145 lines
6.1 KiB
PHP
145 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Mail\ContactRequestMail;
|
|
use App\Models\Customer;
|
|
use App\Models\InboundMail;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\View\View;
|
|
|
|
/**
|
|
* „Sprechen Sie uns an" als Seite, nicht als `mailto:`.
|
|
*
|
|
* Der Knopf unter den Paketen zeigte auf `mailto:`, und der tut auf den meisten
|
|
* Geräten nichts Sichtbares: ohne eingerichtetes Mailprogramm passiert gar
|
|
* nichts, im Browser öffnet sich bestenfalls ein Dialog, den niemand erwartet
|
|
* hat. Das ist ausgerechnet an der Stelle fatal, an der das Selbstbedienen
|
|
* aufhört und ein Gespräch beginnen soll — wer hier klickt, hat sich schon
|
|
* entschieden zu fragen.
|
|
*
|
|
* Eine Seite statt eines Modals: die öffentliche Website lädt kein Livewire,
|
|
* ein gewöhnlicher Formular-POST kommt also auch dann an, wenn JavaScript
|
|
* scheitert. Und eine Seite hat eine Adresse — sie lässt sich aus einer Mail
|
|
* heraus verlinken, ein Modal nicht.
|
|
*/
|
|
class ContactController extends Controller
|
|
{
|
|
/** Themen, die das Formular kennt. Alles andere wird zu `general`. */
|
|
private const TOPICS = ['enterprise', 'general'];
|
|
|
|
public function show(Request $request): View
|
|
{
|
|
$topic = (string) $request->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'];
|
|
}
|
|
}
|