Skip to content

Commit

Permalink
feat: add command to retry failed sent emails
Browse files Browse the repository at this point in the history
  • Loading branch information
lkeio committed Jun 4, 2024
1 parent ea2c55a commit cead93c
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 79 deletions.
20 changes: 12 additions & 8 deletions database/factories/LogFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,18 @@ public function definition(): array
'variables' => [
'name' => $this->faker->name(),
],
'cc' => new ContactDto(
email: $this->faker->safeEmail(),
name: $this->faker->name(),
),
'bcc' => new ContactDto(
email: $this->faker->safeEmail(),
name: $this->faker->name(),
),
'cc' => [
new ContactDto(
email: $this->faker->safeEmail(),
name: $this->faker->name(),
),
],
'bcc' => [
new ContactDto(
email: $this->faker->safeEmail(),
name: $this->faker->name(),
),
],
];
}
}
83 changes: 83 additions & 0 deletions src/Actions/Logs/ResendEmail.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace MailCarrier\Actions\Logs;

use Filament\Notifications\Notification;
use Illuminate\Support\Collection;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use MailCarrier\Actions\Action;
use MailCarrier\Actions\SendMail;
use MailCarrier\Dto\AttachmentDto;
use MailCarrier\Dto\SendMailDto;
use MailCarrier\Enums\LogStatus;
use MailCarrier\Models\Attachment;
use MailCarrier\Models\Log;
use Throwable;

class ResendEmail extends Action
{
public function run(Log $log, array $data = []): void
{
$attachments = $data['attachments'] ?? Collection::make([]);

// Update the status to Failed just to send email again
if ($log->isSent()) {
$log->update([
'status' => LogStatus::Failed,
]);
}

try {
SendMail::resolve()->run(
new SendMailDto([
'template' => $log->template->slug,
'subject' => $log->subject,
'sender' => $log->sender,
'recipient' => $log->recipient,
'cc' => $log->cc->all(),
'bcc' => $log->bcc->all(),
'variables' => $log->variables,
'trigger' => $log->trigger,
'tags' => $log->tags ?: [],
'metadata' => $log->metadata ?: [],
'attachments' => $log->attachments
->map(
fn (Attachment $attachment) => !$attachment->canBeDownloaded()
? null
: new AttachmentDto(
name: $attachment->name,
content: $attachment->content,
size: $attachment->size
)
)
->filter()
->merge(
Collection::make($attachments)->map(
fn (TemporaryUploadedFile $file) => new AttachmentDto(
name: $file->getClientOriginalName(),
content: base64_encode(file_get_contents($file->getRealPath())),
size: $file->getSize()
)
)
)
->all(),
]),
$log
);
} catch (Throwable $e) {
Notification::make()
->title('Error while sending email')
->body($e->getMessage())
->danger()
->send();

return;
}

Notification::make()
->icon('heroicon-o-paper-airplane')
->title('Email sent correctly')
->success()
->send();
}
}
41 changes: 41 additions & 0 deletions src/Commands/LogRetryCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace MailCarrier\Commands;

use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use MailCarrier\Actions\Logs\ResendEmail;
use MailCarrier\Enums\LogStatus;
use MailCarrier\Models\Log;

class LogRetryCommand extends Command
{
public $signature = 'mailcarrier:log-retry {--date=}';

public $description = 'Retry sending emails from log that failed.';

public function handle(): int
{
$this->info('Retrying failed logs...');

$date = $this->option('date')
? Carbon::parse($this->option('date'))
: Carbon::now()->subDay();

$resendEmailAction = new ResendEmail();

$retriedCount = tap(
Log::query()
->whereDate('created_at', '=', $date)
->where('status', LogStatus::Failed)
->get()
)
->each(fn (Log $log) => $resendEmailAction->run($log))
->count();

$this->info($retriedCount . ' ' . Str::plural('log', $retriedCount) . ' retried.');

return self::SUCCESS;
}
}
2 changes: 2 additions & 0 deletions src/MailCarrierServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Filament\Support\Facades\FilamentAsset;
use Illuminate\Support\Facades\Event;
use MailCarrier\Commands\InstallCommand;
use MailCarrier\Commands\LogRetryCommand;
use MailCarrier\Commands\SocialCommand;
use MailCarrier\Commands\TokenCommand;
use MailCarrier\Commands\UpgradeCommand;
Expand Down Expand Up @@ -43,6 +44,7 @@ public function configurePackage(Package $package): void
SocialCommand::class,
UserCommand::class,
TokenCommand::class,
LogRetryCommand::class,
])
->hasMigrations([
'1_create_users_table',
Expand Down
73 changes: 2 additions & 71 deletions src/Resources/LogResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,19 @@

use Carbon\CarbonInterface;
use Filament\Forms\Components\FileUpload;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Support\Colors\Color;
use Filament\Support\Enums\Alignment;
use Filament\Tables;
use Filament\Tables\Actions\Action as TablesAction;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\View;
use Illuminate\Support\HtmlString;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use MailCarrier\Actions\Logs\GetTriggers;
use MailCarrier\Actions\SendMail;
use MailCarrier\Dto\AttachmentDto;
use MailCarrier\Actions\Logs\ResendEmail;
use MailCarrier\Dto\LogTemplateDto;
use MailCarrier\Dto\SendMailDto;
use MailCarrier\Enums\LogStatus;
use MailCarrier\Facades\MailCarrier;
use MailCarrier\Models\Attachment;
use MailCarrier\Models\Log;
use MailCarrier\Models\Template;
use MailCarrier\Resources\LogResource\Pages;
Expand Down Expand Up @@ -186,7 +180,7 @@ protected static function getTableActions(): array
)
->modalSubmitActionLabel(fn (Log $record) => $record->isFailed() ? 'Retry' : 'Resend')
->modalFooterActionsAlignment(Alignment::Right)
->action(fn (?Log $record, array $data) => $record ? static::resendEmail($record, $data) : null)
->action(fn (?Log $record, array $data) => $record ? (new ResendEmail())->run($record, $data) : null)
->visible(fn (?Log $record) => $record?->status !== LogStatus::Pending),
];
}
Expand Down Expand Up @@ -217,67 +211,4 @@ protected static function getTemplateValue(LogTemplateDto $templateDto, ?Templat
($subtitle ? '<p class="text-xs mt-1 text-slate-300">' . $subtitle . '</p>' : '')
);
}

protected static function resendEmail(Log $log, array $data): void
{
// Update the status to Failed just to send email again
if ($log->isSent()) {
$log->update([
'status' => LogStatus::Failed,
]);
}

try {
SendMail::resolve()->run(
new SendMailDto([
'template' => $log->template->slug,
'subject' => $log->subject,
'sender' => $log->sender,
'recipient' => $log->recipient,
'cc' => $log->cc->all(),
'bcc' => $log->bcc->all(),
'variables' => $log->variables,
'trigger' => $log->trigger,
'tags' => $log->tags ?: [],
'metadata' => $log->metadata ?: [],
'attachments' => $log->attachments
->map(
fn (Attachment $attachment) => !$attachment->canBeDownloaded()
? null
: new AttachmentDto(
name: $attachment->name,
content: $attachment->content,
size: $attachment->size
)
)
->filter()
->merge(
Collection::make($data['attachments'])->map(
fn (TemporaryUploadedFile $file) => new AttachmentDto(
name: $file->getClientOriginalName(),
content: base64_encode(file_get_contents($file->getRealPath())),
size: $file->getSize()
)
)
)
->all(),
]),
$log
);
} catch (\Throwable $e) {
Notification::make()
->title('Error while sending email')
->body($e->getMessage())
->danger()
->send();

return;
}

Notification::make()
->icon('heroicon-o-paper-airplane')
->title('Email sent correctly')
->success()
->send();
}
}
92 changes: 92 additions & 0 deletions tests/Feature/Logs/LogRetryCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

use Carbon\CarbonImmutable as Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
use MailCarrier\Enums\LogStatus;
use MailCarrier\Mail\GenericMail;
use MailCarrier\Models\Log;

beforeEach(function () {
// Disable auth
Config::set('mailcarrier.api_endpoint.auth_guard', null);

Config::set('mailcarrier.queue.force', false);
Mail::fake();
});

it('resends emails from failed logs', function () {
$log = Log::factory()->create([
'status' => LogStatus::Failed,
'created_at' => Carbon::now()->subDay(),
]);

$log2 = Log::factory()->create([ // This log should not be resent
'status' => LogStatus::Failed,
'created_at' => Carbon::now()->subDays(2),
]);

$this->artisan('mailcarrier:log-retry')
->expectsOutput('Retrying failed logs...')
->expectsOutput('1 log retried.')
->assertExitCode(0);

Mail::assertSent(GenericMail::class, function (GenericMail $mail) use ($log) {
$mail->build();

return $mail->hasTo($log->recipient) &&
$mail->hasSubject($log->subject) &&
$mail->hasFrom($log->sender->email);
});

Mail::assertNotSent(GenericMail::class, function (GenericMail $mail) use ($log2) {
$mail->build();

return $mail->hasTo($log2->recipient) &&
$mail->hasSubject($log2->subject) &&
$mail->hasFrom($log2->sender->email);
});

expect($log->refresh()->status)->toBe(LogStatus::Sent);
expect($log->error)->toBeNull();

expect($log2->refresh()->status)->toBe(LogStatus::Failed);
});

it('resends emails from failed logs with a specific date', function () {
$log = Log::factory()->create([
'status' => LogStatus::Failed,
'created_at' => '2021-01-01 00:00:00',
]);

$log2 = Log::factory()->create([ // This log should not be resent
'status' => LogStatus::Failed,
'created_at' => '2021-01-03 00:00:00',
]);

$this->artisan('mailcarrier:log-retry --date=2021-01-01')
->expectsOutput('Retrying failed logs...')
->expectsOutput('1 log retried.')
->assertExitCode(0);

Mail::assertSent(GenericMail::class, function (GenericMail $mail) use ($log) {
$mail->build();

return $mail->hasTo($log->recipient) &&
$mail->hasSubject($log->subject) &&
$mail->hasFrom($log->sender->email);
});

Mail::assertNotSent(GenericMail::class, function (GenericMail $mail) use ($log2) {
$mail->build();

return $mail->hasTo($log2->recipient) &&
$mail->hasSubject($log2->subject) &&
$mail->hasFrom($log2->sender->email);
});

expect($log->refresh()->status)->toBe(LogStatus::Sent);
expect($log->error)->toBeNull();

expect($log2->refresh()->status)->toBe(LogStatus::Failed);
});

0 comments on commit cead93c

Please sign in to comment.