Update emails

This commit is contained in:
DariusIII
2026-05-07 14:03:21 +02:00
parent fe08b777f1
commit ac1ac603cb
57 changed files with 1808 additions and 818 deletions
+170
View File
@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Enums\IncidentImpactEnum;
use App\Enums\IncidentStatusEnum;
use App\Mail\AccountChange;
use App\Mail\AccountDeleted;
use App\Mail\AccountExpired;
use App\Mail\AccountWillExpire;
use App\Mail\ContactUs;
use App\Mail\ForgottenPassword;
use App\Mail\IncidentDetected;
use App\Mail\InvitationMail;
use App\Mail\NewAccountCreatedEmail;
use App\Mail\PasswordReset;
use App\Mail\WelcomeEmail;
use App\Models\Invitation;
use App\Models\ServiceIncident;
use App\Models\User;
use App\Notifications\VerifyEmailBranded;
use Illuminate\Console\Command;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Markdown;
use Illuminate\Support\Carbon;
/**
* Generates static HTML previews for every transactional and notification email
* the application can send. Useful for spot-checking the themed templates
* across mail clients without needing to wire up Mailpit or trigger real
* application events. Output lands in `storage/app/mail-previews/*.html`.
*/
class MailPreview extends Command
{
protected $signature = 'mail:preview
{--out= : Override the output directory (defaults to storage/app/mail-previews)}';
protected $description = 'Render every mailable to static HTML files for visual QA across email clients.';
public function handle(): int
{
$directory = (string) ($this->option('out') ?: storage_path('app/mail-previews'));
if (! is_dir($directory) && ! mkdir($directory, 0o755, true) && ! is_dir($directory)) {
$this->components->error("Could not create output directory: {$directory}");
return self::FAILURE;
}
$user = $this->fakeUser();
$invitation = $this->fakeInvitation();
[$activeIncident, $resolvedIncident] = $this->fakeIncidents();
/** @var array<string, callable(): Mailable> $mailables */
$mailables = [
'welcome' => fn (): Mailable => new WelcomeEmail($user),
'forgotten_password' => fn (): Mailable => new ForgottenPassword('https://example.test/reset/abc123'),
'password_reset' => fn (): Mailable => new PasswordReset($user, 'TempPass!2026'),
'new_account_created' => fn (): Mailable => new NewAccountCreatedEmail($user),
'account_change' => fn (): Mailable => new AccountChange($user),
'account_will_expire' => fn (): Mailable => new AccountWillExpire($user, 5),
'account_expired' => fn (): Mailable => new AccountExpired($user),
'account_deleted' => fn (): Mailable => new AccountDeleted($user),
'contact_us' => fn (): Mailable => new ContactUs(
'admin@example.test',
'visitor@example.test',
"Hello!\nQuestion inside.",
),
'invitation' => fn (): Mailable => new InvitationMail($invitation),
'incident_detected' => fn (): Mailable => new IncidentDetected($activeIncident, false),
'incident_resolved' => fn (): Mailable => new IncidentDetected($resolvedIncident, true),
];
foreach ($mailables as $name => $factory) {
$html = $factory()->render();
file_put_contents("{$directory}/{$name}.html", $html);
$this->components->info("Wrote {$name}.html (".number_format(strlen($html)).' bytes)');
}
$this->writeVerifyPreview($directory);
$this->newLine();
$this->components->info("All previews written to: {$directory}");
return self::SUCCESS;
}
private function writeVerifyPreview(string $directory): void
{
$notifiable = new class
{
public int $id = 7;
public string $email = 'tester@example.test';
public string $username = 'tester';
public function getKey(): int
{
return $this->id;
}
public function getEmailForVerification(): string
{
return $this->email;
}
};
$message = (new VerifyEmailBranded)->toMail($notifiable);
$markdown = app(Markdown::class);
$html = (string) $markdown->render($message->markdown, $message->data());
file_put_contents("{$directory}/verify_email.html", $html);
$this->components->info('Wrote verify_email.html ('.number_format(strlen($html)).' bytes)');
}
private function fakeUser(): User
{
$user = new User;
$user->id = 99;
$user->username = 'tester';
$user->email = 'tester@example.test';
$user->setRelation('role', new class
{
public string $name = 'Silver';
});
return $user;
}
private function fakeInvitation(): Invitation
{
$inviter = new User;
$inviter->id = 1;
$inviter->username = 'mod_alice';
$invitation = new Invitation;
$invitation->token = 'preview-token-abc123';
$invitation->email = 'newuser@example.test';
$invitation->expires_at = Carbon::now()->addDays(7);
$invitation->setRelation('invitedBy', $inviter);
return $invitation;
}
/**
* @return array{0: ServiceIncident, 1: ServiceIncident}
*/
private function fakeIncidents(): array
{
$services = collect([
(object) ['name' => 'NZB Search'],
(object) ['name' => 'API'],
]);
$active = new ServiceIncident;
$active->title = 'API latency spike';
$active->description = "p99 latency exceeded thresholds.\nFollow status page for updates.";
$active->status = IncidentStatusEnum::Identified;
$active->impact = IncidentImpactEnum::Critical;
$active->started_at = Carbon::now()->subMinutes(15);
$active->setRelation('services', $services);
$resolved = clone $active;
$resolved->status = IncidentStatusEnum::Resolved;
$resolved->resolved_at = Carbon::now();
return [$active, $resolved];
}
}
-43
View File
@@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Mail\SendInvite;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendInviteEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private string $email;
private string $url;
private User $user;
/**
* SendInviteEmail constructor.
*/
public function __construct(string $email, User $user, string $url)
{
$this->email = $email;
$this->user = $user;
$this->url = $url;
}
/**
* Execute the job.
*/
public function handle(): void
{
Mail::to($this->email)->send(new SendInvite($this->user, $this->url));
}
}
+22 -23
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Mail;
use App\Mail\Concerns\HasBrandedSubject;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,41 +12,39 @@ use Illuminate\Queue\SerializesModels;
class AccountChange extends Mailable
{
use Queueable, SerializesModels;
use HasBrandedSubject, Queueable, SerializesModels;
/**
* @var User
*/
public $user;
public string $username;
/**
* @var mixed
*/
private $siteEmail;
public string $account;
/**
* @var mixed
*/
private $siteTitle;
public string $site;
public ?string $preheader;
private string $siteEmail;
/**
* AccountChange constructor.
*/
public function __construct(User $user)
{
$this->user = $user;
$this->siteEmail = config('mail.from.address');
$this->siteTitle = config('app.name');
$this->username = (string) $user->username;
$this->account = (string) ($user->role->name ?? 'User');
$this->siteEmail = (string) config('mail.from.address');
$this->site = (string) config('app.name');
$this->preheader = "Your {$this->site} account level was updated to {$this->account}.";
}
/**
* Build the message.
*
*
* @throws \Exception
*/
public function build(): static
{
return $this->from($this->siteEmail)->subject('Account Changed')->view('emails.accountChange')->with(['account' => $this->user->role->name, 'username' => $this->user->username, 'site' => $this->siteTitle]);
return $this->from($this->siteEmail)
->brandedSubject('Account level changed')
->markdown('emails.markdown.accountChange', [
'username' => $this->username,
'account' => $this->account,
'site' => $this->site,
'preheader' => $this->preheader,
]);
}
}
+18 -21
View File
@@ -4,38 +4,29 @@ declare(strict_types=1);
namespace App\Mail;
use App\Mail\Concerns\HasBrandedSubject;
use Illuminate\Bus\Queueable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class AccountDeleted extends Mailable
{
use Queueable, SerializesModels;
use HasBrandedSubject, Queueable, SerializesModels;
/**
* @var Model|null|object|static
*/
private $user;
public string $username;
/**
* @var mixed
*/
private $siteEmail;
public string $site;
/**
* @var mixed
*/
private $siteTitle;
public ?string $preheader;
private string $siteEmail;
/**
* Create a new message instance.
*/
public function __construct(mixed $user)
{
$this->user = $user;
$this->siteEmail = config('mail.from.address');
$this->siteTitle = config('app.name');
$this->username = (string) $user->username;
$this->siteEmail = (string) config('mail.from.address');
$this->site = (string) config('app.name');
$this->preheader = "User {$this->username} deleted their {$this->site} account.";
}
/**
@@ -43,6 +34,12 @@ class AccountDeleted extends Mailable
*/
public function build(): static
{
return $this->from($this->siteEmail)->subject('User Account Deleted')->view('emails.accountDelete')->with(['username' => $this->user->username, 'site' => $this->siteTitle]);
return $this->from($this->siteEmail)
->brandedSubject('User account deleted')
->markdown('emails.markdown.accountDeleted', [
'username' => $this->username,
'site' => $this->site,
'preheader' => $this->preheader,
]);
}
}
+22 -14
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Mail;
use App\Mail\Concerns\HasBrandedSubject;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,32 +12,39 @@ use Illuminate\Queue\SerializesModels;
class AccountExpired extends Mailable
{
use Queueable, SerializesModels;
use HasBrandedSubject, Queueable, SerializesModels;
public User $user;
public string $username;
private mixed $siteEmail;
public string $account;
private mixed $siteTitle;
public string $site;
public ?string $preheader;
private string $siteEmail;
/**
* Create a new message instance.
*/
public function __construct(User $user)
{
$this->user = $user;
$this->siteEmail = config('mail.from.address');
$this->siteTitle = config('app.name');
$this->username = (string) $user->username;
$this->account = (string) ($user->role->name ?? 'User');
$this->siteEmail = (string) config('mail.from.address');
$this->site = (string) config('app.name');
$this->preheader = "Your {$this->site} subscription has expired — account downgraded to {$this->account}.";
}
/**
* Build the message.
*
*
* @throws \Exception
*/
public function build(): static
{
return $this->from($this->siteEmail)->subject('Account expired')->view('emails.accountExpired')->with(['account' => $this->user->role->name, 'username' => $this->user->username, 'site' => $this->siteTitle]);
return $this->from($this->siteEmail)
->brandedSubject('Your account has expired')
->markdown('emails.markdown.accountExpired', [
'username' => $this->username,
'account' => $this->account,
'site' => $this->site,
'preheader' => $this->preheader,
]);
}
}
+24 -15
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Mail;
use App\Mail\Concerns\HasBrandedSubject;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,32 +12,40 @@ use Illuminate\Queue\SerializesModels;
class AccountWillExpire extends Mailable
{
use Queueable, SerializesModels;
use HasBrandedSubject, Queueable, SerializesModels;
private int $days;
public int $days;
private User $user;
public string $username;
private mixed $siteEmail;
public string $account;
private mixed $siteTitle;
public string $site;
public ?string $preheader;
private string $siteEmail;
/**
* Create a new message instance.
*/
public function __construct(User $user, int $days)
{
$this->user = $user;
$this->days = $days;
$this->siteEmail = config('mail.from.address');
$this->siteTitle = config('app.name');
$this->username = (string) $user->username;
$this->account = (string) ($user->role->name ?? 'User');
$this->siteEmail = (string) config('mail.from.address');
$this->site = (string) config('app.name');
$this->preheader = "Your {$this->account} role expires in {$this->days} day(s).";
}
/**
* Build the message.
*/
public function build(): static
{
return $this->from($this->siteEmail)->subject('Account about to expire')->view('emails.accountAboutToExpire')->with(['account' => $this->user->role->name, 'username' => $this->user->username, 'site' => $this->siteTitle, 'days' => $this->days]);
return $this->from($this->siteEmail)
->brandedSubject('Your account is about to expire')
->markdown('emails.markdown.accountWillExpire', [
'username' => $this->username,
'account' => $this->account,
'days' => $this->days,
'site' => $this->site,
'preheader' => $this->preheader,
]);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Mail\Concerns;
/**
* Adds a configurable, site-wide subject prefix (default "[App Name] ") to
* mailables, so transactional emails read consistently in users' inboxes.
*
* Concrete mailables call `$this->brandedSubject('Welcome')` instead of
* `$this->subject('Welcome')`. The prefix can be overridden globally via
* `MAIL_SUBJECT_PREFIX` (see `config/mail.php` -> `brand.subject_prefix`)
* or disabled per call by passing `prefix: false`.
*/
trait HasBrandedSubject
{
protected function brandedSubject(string $subject, bool $prefix = true): static
{
$prefixValue = $prefix
? (string) config('mail.brand.subject_prefix', '')
: '';
return $this->subject($prefixValue.$subject);
}
}
+20 -11
View File
@@ -4,13 +4,14 @@ declare(strict_types=1);
namespace App\Mail;
use App\Mail\Concerns\HasBrandedSubject;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class ContactUs extends Mailable
{
use Queueable, SerializesModels;
use HasBrandedSubject, Queueable, SerializesModels;
public string $mailFrom;
@@ -18,24 +19,32 @@ class ContactUs extends Mailable
public string $mailTo;
/**
* Create a new message instance.
*/
public string $site;
public ?string $preheader;
public function __construct(mixed $mailTo, mixed $mailFrom, mixed $mailBody)
{
$this->mailTo = $mailTo;
$this->mailFrom = $mailFrom;
$this->mailBody = $mailBody;
$this->mailTo = (string) $mailTo;
$this->mailFrom = (string) $mailFrom;
$this->mailBody = (string) $mailBody;
$this->site = (string) config('app.name');
$this->preheader = "New contact form submission from {$this->mailFrom}.";
}
/**
* Build the message.
*
*
* @throws \Exception
*/
public function build(): static
{
return $this->from($this->mailTo)->subject('Contact form submitted')->replyTo($this->mailFrom)->view('emails.contactUs')->with(['mailBody' => $this->mailBody]);
return $this->from($this->mailTo)
->replyTo($this->mailFrom)
->brandedSubject('Contact form submitted')
->markdown('emails.markdown.contactUs', [
'mailBody' => $this->mailBody,
'mailFrom' => $this->mailFrom,
'site' => $this->site,
'preheader' => $this->preheader,
]);
}
}
+16 -14
View File
@@ -4,41 +4,43 @@ declare(strict_types=1);
namespace App\Mail;
use App\Mail\Concerns\HasBrandedSubject;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class ForgottenPassword extends Mailable
{
use Queueable, SerializesModels;
public mixed $user;
use HasBrandedSubject, Queueable, SerializesModels;
public string $resetLink;
private mixed $siteEmail;
public string $site;
private mixed $siteTitle;
public ?string $preheader;
private string $siteEmail;
/**
* Create a new message instance.
*/
public function __construct(string $resetLink)
{
$this->resetLink = $resetLink;
$this->siteEmail = config('mail.from.address');
$this->siteEmail = (string) config('mail.from.address');
$siteName = config('app.name');
$this->siteTitle = is_array($siteName) ? (string) ($siteName[0] ?? '') : (string) ($siteName ?? '');
$this->site = is_array($siteName) ? (string) ($siteName[0] ?? '') : (string) ($siteName ?? '');
$this->preheader = 'Reset your password using the secure link inside.';
}
/**
* Build the message.
*
*
* @throws \Exception
*/
public function build(): static
{
return $this->from($this->siteEmail)->subject('Forgotten password reset')->view('emails.forgottenPassword')->with(['resetLink' => $this->resetLink, 'site' => $this->siteTitle]);
return $this->from($this->siteEmail)
->brandedSubject('Reset your password')
->markdown('emails.markdown.forgottenPassword', [
'resetLink' => $this->resetLink,
'site' => $this->site,
'preheader' => $this->preheader,
]);
}
}
+21 -9
View File
@@ -4,34 +4,46 @@ declare(strict_types=1);
namespace App\Mail;
use App\Mail\Concerns\HasBrandedSubject;
use App\Models\ServiceIncident;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class IncidentDetected extends Mailable
class IncidentDetected extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
use HasBrandedSubject, Queueable, SerializesModels;
public function __construct(
private readonly ServiceIncident $incident,
private readonly bool $resolved = false
) {}
public readonly ServiceIncident $incident,
public readonly bool $resolved = false,
) {
// Incident notifications take priority over routine user mail so they
// are routed onto a dedicated high-priority queue (overridable via env).
$this->onQueue((string) config('mail.brand.incident_queue', 'incidents'));
}
public function build(): static
{
$site = config('app.name');
$site = (string) config('app.name');
$status = $this->resolved ? 'Resolved' : 'Detected';
$services = $this->incident->services->pluck('name')->join(', ');
$services = (string) $this->incident->services->pluck('name')->join(', ');
$impact = ucfirst($this->incident->impact->value);
$preheader = $this->resolved
? "{$impact} incident on {$services} has been resolved."
: "{$impact} impact incident detected on {$services}.";
return $this->from(config('mail.from.address'))
->subject("[{$site}] Service Incident {$status}: {$services}")
return $this->from((string) config('mail.from.address'))
->brandedSubject("Service incident {$status}: {$services}")
->view('emails.incidentDetected')
->text('emails.text.incidentDetected')
->with([
'incident' => $this->incident,
'services' => $services,
'resolved' => $this->resolved,
'site' => $site,
'preheader' => $preheader,
'statusUrl' => url('/admin/status'),
]);
}
+14 -15
View File
@@ -4,58 +4,57 @@ declare(strict_types=1);
namespace App\Mail;
use App\Mail\Concerns\HasBrandedSubject;
use App\Models\Invitation;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class InvitationMail extends Mailable
class InvitationMail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
use HasBrandedSubject, Queueable, SerializesModels;
public Invitation $invitation;
/**
* Create a new message instance.
*/
public function __construct(Invitation $invitation)
{
$this->invitation = $invitation;
$this->onQueue((string) config('mail.brand.queue', 'emails'));
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'You\'re invited to join '.config('app.name'),
from: (string) config('mail.from.address'),
subject: (string) config('mail.brand.subject_prefix', '').'You\'re invited to join '.config('app.name'),
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
$siteName = (string) config('app.name');
$invitedByName = (string) ($this->invitation->invitedBy->username ?? 'A member');
return new Content(
view: 'emails.invitation',
text: 'emails.text.invitation',
with: [
'invitation' => $this->invitation,
'invitedBy' => $this->invitation->invitedBy,
'invitedByName' => $invitedByName,
'registerUrl' => route('register', ['token' => $this->invitation->token]),
'expiresAt' => $this->invitation->expires_at,
'siteName' => config('app.name'),
'siteName' => $siteName,
'preheader' => "{$invitedByName} has invited you to join {$siteName}.",
],
);
}
/**
* Get the attachments for the message.
*
* @return array<int, Attachment>
*/
public function attachments(): array
+18 -26
View File
@@ -4,47 +4,39 @@ declare(strict_types=1);
namespace App\Mail;
use App\Models\User;
use App\Mail\Concerns\HasBrandedSubject;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class NewAccountCreatedEmail extends Mailable
{
use Queueable, SerializesModels;
use HasBrandedSubject, Queueable, SerializesModels;
/**
* @var User
*/
private $user;
public string $username;
/**
* @var mixed
*/
private $siteEmail;
public string $site;
/**
* @var mixed
*/
private $siteTitle;
public ?string $preheader;
private string $siteEmail;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(mixed $user)
{
$this->user = $user;
$this->siteEmail = config('mail.from.address');
$this->siteTitle = config('app.name');
$this->username = (string) $user->username;
$this->siteEmail = (string) config('mail.from.address');
$this->site = (string) config('app.name');
$this->preheader = "New user registered on {$this->site}.";
}
/**
* Build the message.
*/
public function build(): static
{
return $this->from($this->siteEmail)->subject('New account registered')->view('emails.newAccountCreated')->with(['username' => $this->user->username, 'site' => $this->siteTitle]);
return $this->from($this->siteEmail)
->brandedSubject('New account registered')
->markdown('emails.markdown.newAccountCreated', [
'username' => $this->username,
'site' => $this->site,
'preheader' => $this->preheader,
]);
}
}
+21 -28
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Mail;
use App\Mail\Concerns\HasBrandedSubject;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,47 +12,39 @@ use Illuminate\Queue\SerializesModels;
class PasswordReset extends Mailable
{
use Queueable, SerializesModels;
use HasBrandedSubject, Queueable, SerializesModels;
/**
* @var User
*/
public $user;
public string $username;
/**
* @var string
*/
public $newPass;
public string $newPass;
/**
* @var mixed
*/
private $siteEmail;
public string $site;
/**
* @var mixed
*/
private $siteTitle;
public ?string $preheader;
/**
* PasswordReset constructor.
*/
public function __construct(User $user, mixed $newPass)
private string $siteEmail;
public function __construct(User $user, string $newPass)
{
$this->user = $user;
$this->username = (string) $user->username;
$this->newPass = $newPass;
$this->siteEmail = config('mail.from.address');
$this->siteTitle = config('app.name');
$this->siteEmail = (string) config('mail.from.address');
$this->site = (string) config('app.name');
$this->preheader = 'Your password has been reset — temporary credentials inside.';
}
/**
* Build the message.
*
*
* @throws \Exception
*/
public function build(): static
{
return $this->from($this->siteEmail)->subject('Password reset')->view('emails.passwordReset')->with(['newPass' => $this->newPass, 'userName' => $this->user->username, 'site' => $this->siteTitle]);
return $this->from($this->siteEmail)
->brandedSubject('Your password has been reset')
->markdown('emails.markdown.passwordReset', [
'username' => $this->username,
'newPass' => $this->newPass,
'site' => $this->site,
'preheader' => $this->preheader,
]);
}
}
-57
View File
@@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class SendInvite extends Mailable
{
use Queueable, SerializesModels;
/**
* @var User
*/
public $user;
/**
* @var string
*/
public $invite;
/**
* @var mixed
*/
private $siteEmail;
/**
* @var mixed
*/
private $siteTitle;
/**
* SendInvite constructor.
*/
public function __construct(User $user, mixed $invite)
{
$this->user = $user;
$this->invite = $invite;
$this->siteEmail = config('mail.from.address');
$this->siteTitle = config('app.name');
}
/**
* Build the message.
*
*
* @throws \Exception
*/
public function build(): static
{
return $this->from($this->siteEmail)->subject('Invite received')->view('emails.sendinvite')->with(['invite' => $this->invite, 'username' => $this->user['username'], 'site' => $this->siteTitle, 'email' => $this->user['email']]);
}
}
+18 -23
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Mail;
use App\Mail\Concerns\HasBrandedSubject;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,38 +12,32 @@ use Illuminate\Queue\SerializesModels;
class WelcomeEmail extends Mailable
{
use Queueable, SerializesModels;
use HasBrandedSubject, Queueable, SerializesModels;
/**
* @var User
*/
private $user;
public string $username;
/**
* @var mixed
*/
private $siteEmail;
public string $site;
/**
* @var mixed
*/
private $siteTitle;
public ?string $preheader;
private string $siteEmail;
/**
* Create a new message instance.
*/
public function __construct(User $user)
{
$this->user = $user;
$this->siteEmail = config('mail.from.address');
$this->siteTitle = config('app.name');
$this->username = (string) $user->username;
$this->site = (string) config('app.name');
$this->siteEmail = (string) config('mail.from.address');
$this->preheader = "Welcome to {$this->site} — your account is ready.";
}
/**
* Build the message.
*/
public function build(): static
{
return $this->from($this->siteEmail)->subject('Welcome to '.$this->siteTitle)->view('emails.welcome')->with(['username' => $this->user->username, 'site' => $this->siteTitle]);
return $this->from($this->siteEmail)
->brandedSubject('Welcome to '.$this->site)
->markdown('emails.markdown.welcome', [
'username' => $this->username,
'site' => $this->site,
'preheader' => $this->preheader,
]);
}
}
+7 -3
View File
@@ -8,11 +8,11 @@ use App\Enums\SignupError;
use App\Enums\UserRole;
use App\Jobs\SendAccountExpiredEmail;
use App\Jobs\SendAccountWillExpireEmail;
use App\Notifications\VerifyEmailBranded;
use App\Rules\ValidEmailDomain;
use App\Services\InvitationService;
use Carbon\CarbonImmutable;
use Illuminate\Auth\MustVerifyEmail as MustVerifyEmailTrait;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -402,11 +402,15 @@ final class User extends Authenticatable implements HasPasskeys, MustVerifyEmail
}
/**
* Send the standard Laravel email verification notification.
* Send the branded email verification notification.
*
* Uses {@see VerifyEmailBranded} so the verification email renders with
* the same themed Markdown components as the rest of the site's
* transactional emails (instead of Laravel's default notification look).
*/
public function sendEmailVerificationNotification(): void
{
$this->notify(new VerifyEmail);
$this->notify(new VerifyEmailBranded);
}
/**
+45
View File
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Notifications;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
/**
* Branded replacement for Laravel's framework `VerifyEmail` notification.
*
* Renders through our themed Markdown mail components (see
* `resources/views/vendor/mail/*` and `resources/views/emails/markdown/verify.blade.php`)
* so verification emails match the rest of the transactional email branding
* instead of falling back to the default Laravel notification look.
*/
class VerifyEmailBranded extends VerifyEmail implements ShouldQueue
{
use Queueable;
public function __construct()
{
$this->onQueue((string) config('mail.brand.queue', 'emails'));
}
public function toMail($notifiable): MailMessage
{
$url = $this->verificationUrl($notifiable);
$site = (string) config('app.name');
$prefix = (string) config('mail.brand.subject_prefix', '');
$username = (string) ($notifiable->username ?? $notifiable->name ?? '');
return (new MailMessage)
->subject($prefix.'Verify your email address')
->markdown('emails.markdown.verify', [
'url' => $url,
'site' => $site,
'username' => $username,
'preheader' => "Confirm your email address to activate your {$site} account.",
]);
}
}