better emails

This commit is contained in:
Andras Bacsai 2023-09-01 15:52:18 +02:00
parent 76510b8971
commit 3fa53556f4
28 changed files with 374 additions and 139 deletions

View File

@ -0,0 +1,184 @@
<?php
namespace App\Console\Commands;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess;
use App\Notifications\Application\StatusChanged;
use App\Notifications\Database\BackupFailed;
use App\Notifications\Database\BackupSuccess;
use App\Notifications\Test;
use App\Notifications\TransactionalEmails\InvitationLink;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Mail;
use Str;
use function Laravel\Prompts\select;
class TestEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'email:test';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send a test email to the admin';
/**
* Execute the console command.
*/
private ?MailMessage $mail = null;
public function handle()
{
$email = select(
'Which Email should be sent?',
options: [
'emails-test' => 'Test',
'application-deployment-success' => 'Application - Deployment Success',
'application-deployment-failed' => 'Application - Deployment Failed',
'application-status-changed' => 'Application - Status Changed',
'backup-success' => 'Database - Backup Success',
'backup-failed' => 'Database - Backup Failed',
'invitation-link' => 'Invitation Link',
'waitlist-invitation-link' => 'Waitlist Invitation Link',
'waitlist-confirmation' => 'Waitlist Confirmation',
],
);
$type = set_transanctional_email_settings();
if (!$type) {
throw new Exception('No email settings found.');
}
$this->mail = new MailMessage();
$this->mail->subject("Test Email");
switch ($email) {
case 'emails-test':
$this->mail = (new Test())->toMail();
break;
case 'application-deployment-success':
$application = Application::all()->first();
$this->mail = (new DeploymentSuccess($application, 'test'))->toMail();
$this->sendEmail();
break;
case 'application-deployment-failed':
$application = Application::all()->first();
$preview = ApplicationPreview::all()->first();
if (!$preview) {
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 1,
'pull_request_html_url' => 'http://example.com',
'fqdn' => $application->fqdn,
]);
}
$this->mail = (new DeploymentFailed($application, 'test'))->toMail();
$this->sendEmail();
$this->mail = (new DeploymentFailed($application, 'test', $preview))->toMail();
$this->sendEmail();
break;
case 'application-status-changed':
$application = Application::all()->first();
$this->mail = (new StatusChanged($application))->toMail();
$this->sendEmail();
break;
case 'backup-failed':
$backup = ScheduledDatabaseBackup::all()->first();
$db = StandalonePostgresql::all()->first();
if (!$backup) {
$backup = ScheduledDatabaseBackup::create([
'enabled' => true,
'frequency' => 'daily',
'save_s3' => false,
'database_id' => $db->id,
'database_type' => $db->getMorphClass(),
'team_id' => 0,
]);
}
$output = 'Because of an error, the backup of the database ' . $db->name . ' failed.';
$this->mail = (new BackupFailed($backup, $db, $output))->toMail();
$this->sendEmail();
break;
case 'backup-success':
$backup = ScheduledDatabaseBackup::all()->first();
$db = StandalonePostgresql::all()->first();
if (!$backup) {
$backup = ScheduledDatabaseBackup::create([
'enabled' => true,
'frequency' => 'daily',
'save_s3' => false,
'database_id' => $db->id,
'database_type' => $db->getMorphClass(),
'team_id' => 0,
]);
}
$this->mail = (new BackupSuccess($backup, $db))->toMail();
$this->sendEmail();
break;
case 'invitation-link':
$user = User::all()->first();
$invitation = TeamInvitation::whereEmail($user->email)->first();
if (!$invitation) {
$invitation = TeamInvitation::create([
'uuid' => Str::uuid(),
'email' => $user->email,
'team_id' => 1,
'link' => 'http://example.com',
]);
}
$this->mail = (new InvitationLink($user))->toMail();
$this->sendEmail();
break;
case 'waitlist-invitation-link':
$this->mail = new MailMessage();
$this->mail->view('emails.waitlist-invitation', [
'email' => 'test2@example.com',
'password' => "supersecretpassword",
]);
$this->mail->subject('Congratulations! You are invited to join Coolify Cloud.');
$this->sendEmail();
break;
case 'waitlist-confirmation':
$this->mail = new MailMessage();
$this->mail->view(
'emails.waitlist-confirmation',
[
'confirmation_url' => 'http://example.com',
'cancel_url' => 'http://example.com',
]
);
$this->mail->subject('You are on the waitlist!');
$this->sendEmail();
break;
}
}
private function sendEmail()
{
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
'internal@example.com',
'Test Email',
)
->to('test@example.com')
->subject($this->mail->subject)
->html((string)$this->mail->render())
);
}
}

View File

@ -4,15 +4,7 @@
class ApplicationPreview extends BaseModel class ApplicationPreview extends BaseModel
{ {
protected $fillable = [ protected $guarded = [];
'uuid',
'pull_request_id',
'pull_request_html_url',
'pull_request_issue_comment_id',
'fqdn',
'status',
'application_id',
];
static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id) static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
{ {

View File

@ -18,15 +18,15 @@ class DeploymentFailed extends Notification implements ShouldQueue
public Application $application; public Application $application;
public string $deployment_uuid; public string $deployment_uuid;
public ApplicationPreview|null $preview; public ?ApplicationPreview $preview = null;
public string $application_name; public string $application_name;
public string|null $deployment_url = null; public ?string $deployment_url = null;
public string $project_uuid; public string $project_uuid;
public string $environment_name; public string $environment_name;
public string|null $fqdn; public ?string $fqdn = null;
public function __construct(Application $application, string $deployment_uuid, ApplicationPreview|null $preview) public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null)
{ {
$this->application = $application; $this->application = $application;
$this->deployment_uuid = $deployment_uuid; $this->deployment_uuid = $deployment_uuid;
@ -67,9 +67,8 @@ public function toMail(): MailMessage
$mail->subject('❌ Deployment failed of ' . $this->application_name . '.'); $mail->subject('❌ Deployment failed of ' . $this->application_name . '.');
} else { } else {
$fqdn = $this->preview->fqdn; $fqdn = $this->preview->fqdn;
$mail->subject('❌ Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' deployment failed.'); $mail->subject('❌ Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.');
} }
$mail->view('emails.application-deployment-failed', [ $mail->view('emails.application-deployment-failed', [
'name' => $this->application_name, 'name' => $this->application_name,
'fqdn' => $fqdn, 'fqdn' => $fqdn,

View File

@ -20,8 +20,7 @@ public function send(SendsEmail $notifiable, Notification $notification): void
} }
$mailMessage = $notification->toMail($notifiable); $mailMessage = $notification->toMail($notifiable);
if ($this->isResend) { // if ($this->isResend) {
foreach ($recepients as $receipient) {
Mail::send( Mail::send(
[], [],
[], [],
@ -30,25 +29,24 @@ public function send(SendsEmail $notifiable, Notification $notification): void
data_get($notifiable, 'smtp_from_address'), data_get($notifiable, 'smtp_from_address'),
data_get($notifiable, 'smtp_from_name'), data_get($notifiable, 'smtp_from_name'),
) )
->to($receipient) ->to($recepients)
->subject($mailMessage->subject) ->subject($mailMessage->subject)
->html((string)$mailMessage->render()) ->html((string)$mailMessage->render())
); );
} // } else {
} else { // Mail::send(
Mail::send( // [],
[], // [],
[], // fn (Message $message) => $message
fn (Message $message) => $message // ->from(
->from( // data_get($notifiable, 'smtp_from_address'),
data_get($notifiable, 'smtp_from_address'), // data_get($notifiable, 'smtp_from_name'),
data_get($notifiable, 'smtp_from_name'), // )
) // ->bcc($recepients)
->bcc($recepients) // ->subject($mailMessage->subject)
->subject($mailMessage->subject) // ->html((string)$mailMessage->render())
->html((string)$mailMessage->render()) // );
); // }
}
} }
private function bootConfigs($notifiable): void private function bootConfigs($notifiable): void

View File

@ -26,7 +26,7 @@ public function send(User $notifiable, Notification $notification): void
} }
$this->bootConfigs(); $this->bootConfigs();
$mailMessage = $notification->toMail($notifiable); $mailMessage = $notification->toMail($notifiable);
if ($this->isResend) { // if ($this->isResend) {
Mail::send( Mail::send(
[], [],
[], [],
@ -39,20 +39,20 @@ public function send(User $notifiable, Notification $notification): void
->subject($mailMessage->subject) ->subject($mailMessage->subject)
->html((string)$mailMessage->render()) ->html((string)$mailMessage->render())
); );
} else { // } else {
Mail::send( // Mail::send(
[], // [],
[], // [],
fn (Message $message) => $message // fn (Message $message) => $message
->from( // ->from(
data_get($settings, 'smtp_from_address'), // data_get($settings, 'smtp_from_address'),
data_get($settings, 'smtp_from_name'), // data_get($settings, 'smtp_from_name'),
) // )
->bcc($email) // ->bcc($email)
->subject($mailMessage->subject) // ->subject($mailMessage->subject)
->html((string)$mailMessage->render()) // ->html((string)$mailMessage->render())
); // );
} // }
} }
private function bootConfigs(): void private function bootConfigs(): void

View File

@ -14,12 +14,13 @@ class BackupFailed extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public string $message = 'Backup FAILED'; public string $name;
public string $frequency;
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output) public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output)
{ {
$this->message = "❌ Database backup for {$database->name} with frequency of $backup->frequency was FAILED.\n\nReason: $output"; $this->name = $database->name;
$this->frequency = $backup->frequency;
} }
public function via(object $notifiable): array public function via(object $notifiable): array
@ -36,20 +37,23 @@ public function via(object $notifiable): array
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
} }
ray($channels);
return $channels; return $channels;
} }
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject("❌ Backup FAILED for {$this->database->name}"); $mail->subject("❌ [ACTION REQUIRED] Backup FAILED for {$this->database->name}");
$mail->line($this->message); $mail->view('emails.backup-failed', [
'name' => $this->name,
'frequency' => $this->frequency,
'output' => $this->output,
]);
return $mail; return $mail;
} }
public function toDiscord(): string public function toDiscord(): string
{ {
return $this->message; return "❌ Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
} }
} }

View File

@ -14,12 +14,13 @@ class BackupSuccess extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public string $message = 'Backup Success'; public string $name;
public string $frequency;
public function __construct(ScheduledDatabaseBackup $backup, public $database) public function __construct(ScheduledDatabaseBackup $backup, public $database)
{ {
$this->message = "✅ Database backup for {$database->name} with frequency of $backup->frequency was successful."; $this->name = $database->name;
$this->frequency = $backup->frequency;
} }
public function via(object $notifiable): array public function via(object $notifiable): array
@ -42,13 +43,16 @@ public function via(object $notifiable): array
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject("✅ Backup success for {$this->database->name}"); $mail->subject("✅ Backup successfully done for {$this->database->name}");
$mail->line($this->message); $mail->view('emails.backup-success', [
'name' => $this->name,
'frequency' => $this->frequency,
]);
return $mail; return $mail;
} }
public function toDiscord(): string public function toDiscord(): string
{ {
return $this->message; return "✅ Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
} }
} }

View File

@ -36,7 +36,7 @@ public function via(object $notifiable): array
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject("Coolify Test Notification"); $mail->subject("Test Email");
$mail->view('emails.test'); $mail->view('emails.test');
return $mail; return $mail;
} }

View File

@ -20,16 +20,19 @@ public function via(): array
return [TransactionalEmailChannel::class]; return [TransactionalEmailChannel::class];
} }
public function toMail(User $user): MailMessage public function __construct(public User $user)
{ {
$invitation = TeamInvitation::whereEmail($user->email)->first(); }
public function toMail(): MailMessage
{
$invitation = TeamInvitation::whereEmail($this->user->email)->first();
$invitation_team = Team::find($invitation->team->id); $invitation_team = Team::find($invitation->team->id);
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject('Invitation for ' . $invitation_team->name); $mail->subject('Invitation for ' . $invitation_team->name);
$mail->view('emails.invitation-link', [ $mail->view('emails.invitation-link', [
'team' => $invitation_team->name, 'team' => $invitation_team->name,
'email' => $user->email, 'email' => $this->user->email,
'invitation_link' => $invitation->link, 'invitation_link' => $invitation->link,
]); ]);
return $mail; return $mail;

View File

@ -24,7 +24,7 @@ public function via(): array
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject('Test Notification'); $mail->subject('Test Email');
$mail->view('emails.test'); $mail->view('emails.test');
return $mail; return $mail;
} }

View File

@ -15,6 +15,7 @@
"laravel/fortify": "^v1.16.0", "laravel/fortify": "^v1.16.0",
"laravel/framework": "^v10.7.1", "laravel/framework": "^v10.7.1",
"laravel/horizon": "^5.15", "laravel/horizon": "^5.15",
"laravel/prompts": "^0.1.6",
"laravel/sanctum": "^v3.2.1", "laravel/sanctum": "^v3.2.1",
"laravel/tinker": "^v2.8.1", "laravel/tinker": "^v2.8.1",
"laravel/ui": "^4.2", "laravel/ui": "^4.2",

2
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "dbb08df7a80c46ce2b9b9fa397ed71c1", "content-hash": "0603276b60e77cd859fabacdaaf31550",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",

View File

@ -17,7 +17,7 @@
| |
*/ */
'name' => env('APP_NAME', 'Laravel'), 'name' => env('APP_NAME', 'Coolify'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -94,7 +94,7 @@
'users' => [ 'users' => [
'provider' => 'users', 'provider' => 'users',
'table' => 'password_reset_tokens', 'table' => 'password_reset_tokens',
'expire' => 60, 'expire' => 10,
'throttle' => 60, 'throttle' => 60,
], ],
], ],

View File

@ -0,0 +1,6 @@
Thank you.<br>
{{ config('app.name') ?? 'Coolify' }}
{{ Illuminate\Mail\Markdown::parse('---') }}
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io)') }}

View File

@ -0,0 +1 @@
Hello,

View File

@ -0,0 +1,6 @@
<x-emails.header />
{{ Illuminate\Mail\Markdown::parse($slot) }}
<x-emails.footer />

View File

@ -1,8 +1,11 @@
@if ($pull_request_id !== 0) <x-emails.layout>
Pull Request #{{ $pull_request_id }} of {{ $name }} (<a target="_blank" @if ($pull_request_id === 0)
href="{{ $fqdn }}">{{ $fqdn }}</a>) deployment failed: Failed to deploy a new version of {{ $name }} at [{{ $fqdn }}]({{ $fqdn }}) .
@else @else
Deployment failed of {{ $name }} (<a target="_blank" href="{{ $fqdn }}">{{ $fqdn }}</a>): Failed to deploy a pull request #{{ $pull_request_id }} of {{ $name }} at
[{{ $fqdn }}]({{ $fqdn }}).
@endif @endif
<a target="_blank" href="{{ $deployment_url }}">View Deployment Logs</a><br><br> [View Deployment Logs]({{ $deployment_url }})
</x-emails.layout>

View File

@ -1,8 +1,10 @@
<x-emails.layout>
@if ($pull_request_id === 0) @if ($pull_request_id === 0)
A new version of <a target="_blank" href="{{ $fqdn }}">{{ $fqdn }}</a> is available: A new version of {{ $name }} is available at [{{ $fqdn }}]({{ $fqdn }}) .
@else @else
Pull request #{{ $pull_request_id }} of {{ $name }} deployed successfully: <a target="_blank" Pull request #{{ $pull_request_id }} of {{ $name }} deployed successfully [{{ $fqdn }}]({{ $fqdn }}).
href="{{ $fqdn }}">Application Link</a> |
@endif @endif
<a target="_blank" href="{{ $deployment_url }}">View
Deployment Logs</a><br><br> [View Deployment Logs]({{ $deployment_url }})
</x-emails.layout>

View File

@ -1,2 +1,9 @@
{{ $name }} has been stopped.<br><br> <x-emails.layout>
<a target="_blank" href="{{ $application_url }}">Open in Coolify</a><br><br>
{{ $name }} has been stopped.
If it was your intention to stop this application, you can ignore this email.
If not, [check what is going on]({{ $application_url }}).
</x-emails.layout>

View File

@ -0,0 +1,8 @@
<x-emails.layout>
Database backup for {{ $name }} with frequency of {{ $frequency }} was FAILED.
### Reason
{{ $output }}
</x-emails.layout>

View File

@ -0,0 +1,3 @@
<x-emails.layout>
Database backup for {{ $name }} with frequency of {{ $frequency }} was successful.
</x-emails.layout>

View File

@ -1,13 +1,11 @@
Hello,<br><br> <x-emails.layout>
You have been invited to "{{ $team }}" on "{{ config('app.name') }}".<br><br> You have been invited to "{{ $team }}" on "{{ config('app.name') }}".
Please click here to accept the invitation: <a target="_blank" href="{{ $invitation_link }}">Accept Invitation</a><br> Please [click here]({{ $invitation_link }}) to accept the invitation.
<br>
If you have any questions, please contact the team owner.<br><br> If you have any questions, please contact the team owner.<br><br>
If it was not you who requested this invitation, please ignore this ema il, or instantly revoke the invitation by If it was not you who requested this invitation, please ignore this email, or instantly revoke the invitation by clicking [here]({{ $invitation_link }}/revoke).
clicking here: <a target="_blank" href="{{ $invitation_link }}/revoke">Revoke Invitation</a><br><br>
Thank you. </x-emails.layout>

View File

@ -1,6 +1,7 @@
A password reset requested for your email address on "{{ config('app.name') }}".<br><br> <x-emails.layout>
A password reset has been requested for this email address on [{{ config('app.name') }}]({{ config('app.url') }}).
Please click the following link to reset your password: <a target="_blank" href="{{ $url }}">Password Click [here]({{ $url }}) to reset your password.
Reset</a><br><br>
This password reset link will expire in {{ $count }} minutes. This link will expire in {{ $count }} minutes.
</x-emails.layout>

View File

@ -1,4 +1,6 @@
Your last invoice has failed to be paid for Coolify Cloud. Please <a href="{{$stripeCustomerPortal}}">update payment details on your Stripe Customer Portal</a>. <x-emails.layout>
<br><br> Your last invoice has failed to be paid for Coolify Cloud.
Thanks,<br>
Coolify Cloud Please update payment details [here]({{$stripeCustomerPortal}}).
</x-emails.layout>

View File

@ -1 +1,3 @@
If you are seeing this, it means that your SMTP settings are correct. <x-emails.layout>
If you are seeing this, it means that your Email settings are correct.
</x-emails.layout>

View File

@ -1,4 +1,9 @@
Someone added this email to the Coolify Cloud's waitlist. <x-emails.layout>
<br> Someone added this email to the Coolify Cloud's waitlist. [Click here]({{ $confirmation_url }}) to confirm!
<a href="{{ $confirmation_url }}">Click here to confirm</a>! The link will expire in {{config('constants.waitlist.expiration')}} minutes.<br><br>
You have no idea what <a href="https://coolify.io">Coolify Cloud</a> is or this waitlist? <a href="{{ $cancel_url }}">Click here to remove</a> you from the waitlist. The link will expire in {{config('constants.waitlist.expiration')}} minutes.
You have no idea what [Coolify Cloud](https://coolify.io) is or this waitlist? [Click here]({{ $cancel_url }}) to remove you from the waitlist.
</x-emails.layout>

View File

@ -1,13 +1,19 @@
You have been invited to join the Coolify Cloud. <a href="{{base_url()}}/login">Login here</a> <x-emails.layout>
<br> You have been invited to join the Coolify Cloud.
<br>
[Login here]({{base_url()}}/login)
Here is your initial login information. Here is your initial login information.
<br>
Email: <br> Email:
{{ $email }}
<br><br> **{{ $email }}**
Password:<br>
{{ $password }} Initial Password:
<br><br>
**{{ $password }}**
(You will forced to change it on first login.) (You will forced to change it on first login.)
</x-emails.layout>