From 5b7bb88767d4e2f3fe3db915afe162b3371ce46d Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sun, 12 Jul 2026 20:08:04 +0000 Subject: [PATCH] Use organization logo in Meet outbound emails. Replace the product email mark with company logo or name in contact-message mail. Co-authored-by: Cursor --- app/Mail/ContactMessageMail.php | 4 + app/Services/Comms/EmailService.php | 13 ++- app/Services/Meet/MeetNotificationService.php | 12 +++ app/Support/OrganizationBranding.php | 23 ++++ .../views/email/contact-message.blade.php | 16 ++- .../views/email/customer-branded.blade.php | 22 ++++ resources/views/meet/settings/edit.blade.php | 14 ++- tests/Feature/MeetEmailBrandingTest.php | 100 ++++++++++++++++++ 8 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 resources/views/email/customer-branded.blade.php create mode 100644 tests/Feature/MeetEmailBrandingTest.php diff --git a/app/Mail/ContactMessageMail.php b/app/Mail/ContactMessageMail.php index 8081ad8..842ccad 100644 --- a/app/Mail/ContactMessageMail.php +++ b/app/Mail/ContactMessageMail.php @@ -17,6 +17,8 @@ class ContactMessageMail extends Mailable public string $bodyText, public ?string $fromName = null, public ?string $replyToAddress = null, + public ?string $logoUrl = null, + public ?string $companyName = null, ) {} public function envelope(): Envelope @@ -34,6 +36,8 @@ class ContactMessageMail extends Mailable with: [ 'bodyText' => $this->bodyText, 'fromName' => $this->fromName, + 'logoUrl' => $this->logoUrl, + 'companyName' => $this->companyName, ], ); } diff --git a/app/Services/Comms/EmailService.php b/app/Services/Comms/EmailService.php index bf33e0b..fc89c94 100644 --- a/app/Services/Comms/EmailService.php +++ b/app/Services/Comms/EmailService.php @@ -3,6 +3,8 @@ namespace App\Services\Comms; use App\Mail\ContactMessageMail; +use App\Models\Organization; +use App\Support\OrganizationBranding; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; @@ -13,14 +15,21 @@ use Illuminate\Support\Facades\Mail; */ class EmailService { - public function send(string $to, string $subject, string $body, ?string $fromName = null, ?string $replyTo = null): bool + public function send(string $to, string $subject, string $body, ?string $fromName = null, ?string $replyTo = null, ?Organization $organization = null): bool { if (! filter_var($to, FILTER_VALIDATE_EMAIL)) { return false; } + $logoUrl = null; + $companyName = null; + if ($organization) { + $logoUrl = OrganizationBranding::emailLogoUrl($organization); + $companyName = $organization->name; + } + try { - Mail::to($to)->send(new ContactMessageMail($subject, $body, $fromName, $replyTo)); + Mail::to($to)->send(new ContactMessageMail($subject, $body, $fromName, $replyTo, $logoUrl, $companyName)); return true; } catch (\Throwable $e) { diff --git a/app/Services/Meet/MeetNotificationService.php b/app/Services/Meet/MeetNotificationService.php index fb5bd33..f6fd987 100644 --- a/app/Services/Meet/MeetNotificationService.php +++ b/app/Services/Meet/MeetNotificationService.php @@ -56,6 +56,9 @@ class MeetNotificationService $invitation->email, 'Invitation: '.$room->title, $this->invitationBody($room, $host, $invitation), + null, + null, + $room->organization, ); } @@ -83,6 +86,9 @@ class MeetNotificationService $host->email, 'Recording ready: '.$recording->session->room->title, 'Your meeting recording is ready to view at '.route('meet.recordings.show', $recording), + null, + null, + $recording->session->room->organization, ); } } @@ -101,6 +107,9 @@ class MeetNotificationService $host->email, 'Reminder: '.$room->title.' starts soon', 'Your meeting starts at '.$room->scheduled_at?->timezone($room->timezone)->format('M j, Y g:i A T')."\n\nJoin: ".$room->joinUrl(), + null, + null, + $room->organization, ); $host->notify(new MeetingReminderNotification($room)); @@ -113,6 +122,9 @@ class MeetNotificationService $invitation->email, 'Reminder: '.$room->title.' starts soon', 'You are invited to a meeting starting at '.$room->scheduled_at?->timezone($room->timezone)->format('M j, Y g:i A T')."\n\nJoin: ".$room->joinUrl(), + null, + null, + $room->organization, ); } } diff --git a/app/Support/OrganizationBranding.php b/app/Support/OrganizationBranding.php index 9b23050..6ccf4d4 100644 --- a/app/Support/OrganizationBranding.php +++ b/app/Support/OrganizationBranding.php @@ -47,4 +47,27 @@ class OrganizationBranding Storage::disk('public')->delete($organization->logo_path); } } + + public static function emailLogoUrl(Organization $organization): ?string + { + if (! self::hasCustomLogo($organization)) { + return null; + } + $ext = strtolower(pathinfo($organization->logo_path, PATHINFO_EXTENSION)); + if ($ext === 'svg') { + return null; + } + $version = $organization->updated_at?->getTimestamp() ?? time(); + + return Storage::disk('public')->url($organization->logo_path).'?v='.$version; + } + + public static function wrapEmailHtml(string $bodyHtml, Organization $organization): string + { + return view('email.customer-branded', [ + 'logoUrl' => self::emailLogoUrl($organization), + 'companyName' => $organization->name, + 'bodyHtml' => $bodyHtml, + ])->render(); + } } diff --git a/resources/views/email/contact-message.blade.php b/resources/views/email/contact-message.blade.php index 1a368a5..d4111db 100644 --- a/resources/views/email/contact-message.blade.php +++ b/resources/views/email/contact-message.blade.php @@ -6,16 +6,24 @@
-
- Ladill Meet -
+ @if (! empty($logoUrl) || ! empty($companyName)) +
+ @if (! empty($logoUrl)) + {{ $companyName }} + @else +
{{ $companyName }}
+ @endif +
+ @endif
{{ $bodyText }}
@if (! empty($fromName))

— {{ $fromName }}

@endif
-

Sent via Ladill Meet

+ @if (! empty($companyName)) +

{{ $companyName }}

+ @endif
diff --git a/resources/views/email/customer-branded.blade.php b/resources/views/email/customer-branded.blade.php new file mode 100644 index 0000000..81411c5 --- /dev/null +++ b/resources/views/email/customer-branded.blade.php @@ -0,0 +1,22 @@ + + + + + + + +
+
+ @if (! empty($logoUrl)) + {{ $companyName }} + @else +
{{ $companyName }}
+ @endif +
+
+ {!! $bodyHtml !!} +
+

{{ $companyName }}

+
+ + diff --git a/resources/views/meet/settings/edit.blade.php b/resources/views/meet/settings/edit.blade.php index 37d6d80..0ba6a4a 100644 --- a/resources/views/meet/settings/edit.blade.php +++ b/resources/views/meet/settings/edit.blade.php @@ -30,11 +30,19 @@ @if ($canManage)
- - + +

PNG, JPG, WebP, or SVG up to 2 MB. Shown in meeting emails.

@if (\App\Support\OrganizationBranding::hasCustomLogo($organization)) - + {{ $organization->name }} + @endif +
diff --git a/tests/Feature/MeetEmailBrandingTest.php b/tests/Feature/MeetEmailBrandingTest.php new file mode 100644 index 0000000..7b63ed7 --- /dev/null +++ b/tests/Feature/MeetEmailBrandingTest.php @@ -0,0 +1,100 @@ +withoutMiddleware(EnsurePlatformSession::class); + + $this->user = User::create([ + 'public_id' => 'meet-brand-001', + 'name' => 'Test User', + 'email' => 'meet@example.com', + ]); + + $this->organization = Organization::create([ + 'owner_ref' => $this->user->public_id, + 'name' => 'Meet Corp', + 'slug' => 'meet-corp', + 'timezone' => 'UTC', + 'settings' => ['onboarded' => true], + ]); + + Member::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $this->user->public_id, + 'role' => 'owner', + ]); + } + + public function test_invitation_email_contains_organization_branding(): void + { + Storage::fake('public'); + Mail::fake(); + + $path = UploadedFile::fake()->image('meet-logo.png', 400, 120) + ->store('meet/organizations/'.$this->organization->id, 'public'); + $this->organization->update(['logo_path' => $path, 'name' => 'Meet Corp']); + + $room = Room::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'host_user_ref' => $this->user->public_id, + 'title' => 'Quarterly Review', + 'type' => 'scheduled', + 'status' => 'scheduled', + 'scheduled_at' => now()->addDay(), + 'timezone' => 'UTC', + 'settings' => config('meet.default_settings'), + ]); + + $invitation = Invitation::create([ + 'owner_ref' => $this->user->public_id, + 'room_id' => $room->id, + 'email' => 'guest@example.com', + 'role' => 'participant', + 'status' => 'pending', + 'sent_at' => now(), + ]); + + app(MeetNotificationService::class)->sendInvitationEmail($room, $invitation, $this->user); + + $logoUrl = OrganizationBranding::emailLogoUrl($this->organization->fresh()); + + Mail::assertSent(ContactMessageMail::class, function (ContactMessageMail $mail) use ($logoUrl) { + $html = $mail->render(); + + return $mail->companyName === 'Meet Corp' + && $mail->logoUrl === $logoUrl + && str_contains($html, 'Meet Corp') + && str_contains($html, $logoUrl) + && ! str_contains($html, 'ladillmeet-logo-email') + && ! str_contains($html, 'Sent via Ladill Meet'); + }); + } +}