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>
This commit is contained in:
Kevin Adametz 2026-10-14 10:30:04 +00:00
parent 209a79f173
commit 67016ed549
5 changed files with 59 additions and 5 deletions

View file

@ -7,6 +7,7 @@ use App\Enums\EmailNotificationType;
use App\Mail\Concerns\Unsubscribable;
use App\Models\PressRelease;
use App\Models\User;
use App\Services\Auth\MailAccessLink;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
@ -35,6 +36,16 @@ class PressReleaseHeldForReview extends Mailable
public int $maxAttempts;
/**
* Empfänger ohne Konto (API-Meldung, Roadmap 6.17): Adresse dieser Mail
* und Link, der einen Anmeldelink an sie schickt. Der Link zum Kundenbereich
* führte sonst auf eine Anmeldeseite für ein Konto, das der Empfänger
* nicht kennt.
*/
public ?string $accessEmail = null;
public ?string $accessUrl = null;
public function __construct(
public readonly User $user,
public readonly PressRelease $pressRelease,
@ -55,6 +66,8 @@ class PressReleaseHeldForReview extends Mailable
]);
if ($recipientEmail !== null && mb_strtolower(trim($recipientEmail)) !== mb_strtolower((string) $user->email)) {
$this->accessEmail = trim($recipientEmail);
$this->accessUrl = app(MailAccessLink::class)->url($this->accessEmail, $pressRelease, ConversionSource::MailReview);
$this->configureAddressUnsubscribe($recipientEmail, EmailNotificationType::PressReleaseRejected);
} else {
$this->configureUnsubscribe($user, EmailNotificationType::PressReleaseRejected);

View file

@ -11,7 +11,7 @@ use Illuminate\Support\Facades\URL;
/**
* „Anmeldelink an diese Adresse“ aus einer Kundenmail (Roadmap 6.17).
*
* Veröffentlichungs- und Ablehnungsmails zu API-Meldungen gehen an den
* 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

View file

@ -24,7 +24,21 @@
</p>
</div>
<x-emails.button :url="$showUrl">Stand der Prüfung ansehen</x-emails.button>
@if ($accessUrl)
{{-- Empfänger ohne Konto (API-Meldung, Roadmap 6.17): ein Klick
schickt einen Anmeldelink an genau diese Adresse. --}}
<div class="meta">
<p><strong>Den Stand der Prüfung sehen?</strong></p>
<p>
Dafür brauchen Sie weder ein Konto noch ein Passwort. Wir schicken einen Anmeldelink an
<strong>{{ $accessEmail }}</strong>, die Adresse, an die diese Mail ging. Nach der Anmeldung
sehen Sie die Meldung und ihren Stand. Das ist kostenlos.
</p>
<x-emails.button :url="$accessUrl">Anmeldelink anfordern</x-emails.button>
</div>
@else
<x-emails.button :url="$showUrl">Stand der Prüfung ansehen</x-emails.button>
@endif
<p style="font-size: 12.5px; color: #6b7280;">
Bei Fragen erreichen Sie uns über den <a href="{{ config('domains.domain_portal_url') }}/de/hilfe">Hilfe-Bereich auf pressekonto</a>.

View file

@ -17,8 +17,8 @@ use Livewire\Component;
/**
* Anmeldelink an die Adresse, an die eine Kundenmail ging (Roadmap 6.17).
*
* Erreichbar nur über den signierten Link aus der Veröffentlichungs- oder
* Ablehnungsmail (MailAccessLink). Ein Klick schickt den Anmeldelink; nach
* Erreichbar nur über den signierten Link aus der Veröffentlichungs-,
* Ablehnungs- oder Prüfmail (MailAccessLink). Ein Klick schickt den Anmeldelink; nach
* der Anmeldung landet der Empfänger auf der Meldung. Die Seite zeigt die
* Adresse nur maskiert.
*/
@ -98,7 +98,7 @@ new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Ohne Passwort
{
$source = ConversionSource::parse((string) request()->query('src'));
return in_array($source, [ConversionSource::MailPublish, ConversionSource::MailReject], true) ? $source : ConversionSource::Organic;
return in_array($source, [ConversionSource::MailPublish, ConversionSource::MailReject, ConversionSource::MailReview], true) ? $source : ConversionSource::Organic;
}
private function pressRelease(int $id): ?PressRelease

View file

@ -5,6 +5,7 @@ use App\Enums\ConversionSource;
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Mail\MagicLoginLink;
use App\Mail\PressReleaseHeldForReview;
use App\Mail\PressReleasePublished;
use App\Mail\PressReleaseRejected;
use App\Models\Company;
@ -93,6 +94,32 @@ test('the rejection mail tells recipients without an account that they can revis
->and($own)->not->toContain('Anmeldelink anfordern');
});
test('the held-for-review mail lets recipients without an account see the status via a sign-in link', function () {
/** @var TestCase $this */
$release = apiRelease(['status' => PressReleaseStatus::Review->value]);
$mail = new PressReleaseHeldForReview($release->user, $release, false, 'Frau Muster', 'presse@firma.de');
$html = $mail->render();
expect($mail->accessUrl)->toContain('/zugang?')
->and(accessQuery($mail->accessUrl)['src'])->toBe(ConversionSource::MailReview->value)
->and($html)->toContain('Sie müssen nichts tun')
->and($html)->toContain('Den Stand der Prüfung sehen?')
->and($html)->toContain('weder ein Konto noch ein Passwort')
->and($html)->toContain('presse@firma.de')
->and($html)->toContain('Anmeldelink anfordern')
->and($html)->not->toContain('Stand der Prüfung ansehen');
// Die Zugangsseite übernimmt die Herkunft aus der Prüfmail.
$this->get($mail->accessUrl)->assertOk();
expect(ConversionEvent::query()->where('type', ConversionEventType::ManagePageOpened->value)->sole()->source)
->toBe(ConversionSource::MailReview);
// Wer selbst eingereicht hat, öffnet die Meldung wie bisher im Konto.
$own = (new PressReleaseHeldForReview($release->user, $release, false))->render();
expect($own)->toContain('Stand der Prüfung ansehen')
->and($own)->not->toContain('Anmeldelink anfordern');
});
// --------------------------------------------------------- Zugangsseite
test('the access page only opens with a valid signature and shows the address masked', function () {