presseportale/app/Services/Auth/MailAccessLink.php
Kevin Adametz 67016ed549 mail: Prüfmail führt Pressekontakte per Anmeldelink zur Meldung
Die Mail „Ihre Pressemitteilung wird persönlich geprüft“ verlinkte auch
bei API-Meldungen in den Kundenbereich. Empfänger ohne Konto landeten auf
der Passwort-Anmeldung. Wie in der Veröffentlichungs- und Ablehnungsmail
schickt jetzt ein Klick einen Anmeldelink an die Adresse der Mail; die
Zugangsseite übernimmt die Herkunft mail_review für den Funnel.

Changelog: Verbessert
Public: Auch die Mail zur persönlichen Prüfung führt Pressekontakte ohne Passwort zu ihrer Meldung.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:30:04 +00:00

63 lines
2.2 KiB
PHP

<?php
namespace App\Services\Auth;
use App\Enums\ConversionSource;
use App\Models\PressRelease;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\URL;
/**
* „Anmeldelink an diese Adresse“ aus einer Kundenmail (Roadmap 6.17).
*
* Veröffentlichungs-, Ablehnungs- und Prüfmails zu API-Meldungen gehen an den
* Pressekontakt oder die Firmen-E-Mail — Empfänger, die aus ihrer Sicht kein
* Konto haben. Der Link führt auf eine Seite, die mit einem Klick einen
* Anmeldelink an genau die Adresse schickt, an die die Mail ging, und nach
* der Anmeldung auf der Meldung landet.
*
* Der Link meldet niemanden an: Er löst nur einen Anmeldelink an eine
* Adresse aus, die ihn ohnehin bekommen könnte (`/anmeldelink`). Signiert und
* befristet, die Adresse verschlüsselt — sie steht nicht lesbar in der URL,
* in Logs oder in der Webanalyse. `src` und `pr` stehen zusätzlich im Klartext
* für die Klickmessung (RecordMailLinkClick).
*/
class MailAccessLink
{
public const int VALID_DAYS = 30;
public function url(string $email, PressRelease $pressRelease, ConversionSource $source): string
{
return URL::temporarySignedRoute('mail-access', now()->addDays(self::VALID_DAYS), [
't' => Crypt::encryptString((string) json_encode([
'e' => mb_strtolower(trim($email)),
'p' => $pressRelease->getKey(),
])),
'src' => $source->value,
'pr' => $pressRelease->getKey(),
]);
}
/**
* @return array{email: string, press_release_id: int}|null
*/
public function decode(?string $token): ?array
{
if (blank($token)) {
return null;
}
try {
$data = json_decode(Crypt::decryptString((string) $token), true);
} catch (DecryptException) {
return null;
}
if (! is_array($data) || ! is_string($data['e'] ?? null) || ! filter_var($data['e'], FILTER_VALIDATE_EMAIL) || (int) ($data['p'] ?? 0) <= 0) {
return null;
}
return ['email' => $data['e'], 'press_release_id' => (int) $data['p']];
}
}