From 56c29e465df70044fb2cb845a82b7f7c997e47c5 Mon Sep 17 00:00:00 2001 From: Stijn Jakobs Date: Sat, 30 May 2026 18:13:40 +0200 Subject: [PATCH] Add Resend mailer support --- .env.example | 4 + .../Environment/EmailSettingsCommand.php | 28 ++++- composer.json | 1 + config/mail.php | 6 +- resources/lang/en/command/messages.php | 1 + resources/views/admin/settings/mail.blade.php | 3 + .../Environment/EmailSettingsCommandTest.php | 119 ++++++++++++++++++ 7 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 tests/Integration/Commands/Environment/EmailSettingsCommandTest.php diff --git a/.env.example b/.env.example index bf2cf551e..d633625f2 100644 --- a/.env.example +++ b/.env.example @@ -43,6 +43,10 @@ MAIL_FROM_NAME="Pterodactyl Panel" # @see: https://github.com/pterodactyl/panel/pull/3110 # MAIL_EHLO_DOMAIN=panel.example.com +# Resend API key (required when MAIL_MAILER=resend) +# Get your API key at https://resend.com/api-keys +# RESEND_KEY= + ## # CAPTCHA Configuration # Supported providers: recaptcha, turnstile, none diff --git a/app/Console/Commands/Environment/EmailSettingsCommand.php b/app/Console/Commands/Environment/EmailSettingsCommand.php index 3a211394c..0af5836ac 100644 --- a/app/Console/Commands/Environment/EmailSettingsCommand.php +++ b/app/Console/Commands/Environment/EmailSettingsCommand.php @@ -21,7 +21,8 @@ class EmailSettingsCommand extends Command {--port=} {--endpoint=} {--username=} - {--password=}'; + {--password=} + {--resend-key= : API key for Resend mail transport.}'; protected array $variables = []; @@ -48,6 +49,7 @@ public function handle() 'mailgun' => 'Mailgun Transactional Email', 'mandrill' => 'Mandrill Transactional Email', 'postmark' => 'Postmark Transactional Email', + 'resend' => 'Resend Transactional Email', ], $this->config->get('mail.default', 'smtp') ); @@ -62,6 +64,12 @@ public function handle() $this->config->get('mail.from.address') ); + if (empty($this->variables['MAIL_FROM_ADDRESS'])) { + $this->output->error('A from address is required for sending emails.'); + + return 1; + } + $this->variables['MAIL_FROM_NAME'] = $this->option('from') ?? $this->ask( trans('command/messages.environment.mail.ask_mail_name'), $this->config->get('mail.from.name') @@ -149,4 +157,22 @@ private function setupPostmarkDriverVariables() $this->config->get('mail.username') ); } + + /** + * Handle variables for resend driver. + */ + private function setupResendDriverVariables() + { + $this->variables['MAIL_DRIVER'] = 'resend'; + + $this->variables['RESEND_KEY'] = $this->option('resend-key') ?? $this->ask( + trans('command/messages.environment.mail.ask_resend_key'), + $this->config->get('services.resend.key') + ); + + if (empty($this->variables['RESEND_KEY'])) { + $this->output->error('A Resend API key is required when using the Resend mail driver.'); + $this->output->comment('You can obtain an API key at https://resend.com/api-keys'); + } + } } diff --git a/composer.json b/composer.json index 30636ebd1..bf3c487bc 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "predis/predis": "^3.3.0", "prologue/alerts": "^1.3.0", "psr/cache": "^3.0.0", + "resend/resend-php": "^0.10.0", "s1lentium/iptools": "~1.2.0", "spatie/laravel-fractal": "^6.3.2", "spatie/laravel-query-builder": "^6.3.6", diff --git a/config/mail.php b/config/mail.php index a62b40bda..e16aa4ba8 100644 --- a/config/mail.php +++ b/config/mail.php @@ -28,7 +28,7 @@ | mailers below. You are free to add additional mailers as required. | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", - | "postmark", "log", "array", "failover" + | "postmark", "resend", "log", "array", "failover" | */ @@ -56,6 +56,10 @@ 'transport' => 'postmark', ], + 'resend' => [ + 'transport' => 'resend', + ], + 'sendmail' => [ 'transport' => 'sendmail', 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), diff --git a/resources/lang/en/command/messages.php b/resources/lang/en/command/messages.php index a4a3aafdf..621565dda 100644 --- a/resources/lang/en/command/messages.php +++ b/resources/lang/en/command/messages.php @@ -57,6 +57,7 @@ 'ask_mailgun_secret' => 'Mailgun Secret', 'ask_mandrill_secret' => 'Mandrill Secret', 'ask_postmark_username' => 'Postmark API Key', + 'ask_resend_key' => 'Resend API Key', 'ask_driver' => 'Which driver should be used for sending emails?', 'ask_mail_from' => 'Email address emails should originate from', 'ask_mail_name' => 'Name that emails should appear from', diff --git a/resources/views/admin/settings/mail.blade.php b/resources/views/admin/settings/mail.blade.php index 9e99acd30..a0e418162 100644 --- a/resources/views/admin/settings/mail.blade.php +++ b/resources/views/admin/settings/mail.blade.php @@ -27,6 +27,9 @@
This interface is limited to instances using SMTP as the mail driver. Please either use php artisan p:environment:mail command to update your email settings, or set MAIL_DRIVER=smtp in your environment file. + @if(config('mail.default') === 'resend') +

You are currently using the Resend mail driver. To configure it, set RESEND_KEY in your environment file or run php artisan p:environment:mail. + @endif
diff --git a/tests/Integration/Commands/Environment/EmailSettingsCommandTest.php b/tests/Integration/Commands/Environment/EmailSettingsCommandTest.php new file mode 100644 index 000000000..17c5a44b3 --- /dev/null +++ b/tests/Integration/Commands/Environment/EmailSettingsCommandTest.php @@ -0,0 +1,119 @@ +artisan('p:environment:mail', [ + '--driver' => 'smtp', + '--host' => 'smtp.test.com', + '--port' => '587', + '--username' => 'user@test.com', + '--password' => 'secret', + '--encryption' => 'tls', + '--email' => 'panel@test.com', + '--from' => 'Test Panel', + ])->assertExitCode(0); + + // Verify the .env file was updated + $env = file_get_contents(base_path('.env')); + $this->assertStringContainsString('MAIL_DRIVER=smtp', $env); + $this->assertStringContainsString('MAIL_HOST=smtp.test.com', $env); + $this->assertStringContainsString('MAIL_PORT=587', $env); + $this->assertStringContainsString('MAIL_FROM_ADDRESS=panel@test.com', $env); + } + + /** + * Test that the Resend driver can be selected and configured. + */ + public function test_resend_driver_selection(): void + { + $this->artisan('p:environment:mail', [ + '--driver' => 'resend', + '--resend-key' => 're_test_123456789', + '--email' => 'panel@test.com', + '--from' => 'Test Panel', + ])->assertExitCode(0); + + $env = file_get_contents(base_path('.env')); + $this->assertStringContainsString('MAIL_DRIVER=resend', $env); + $this->assertStringContainsString('RESEND_KEY=re_test_123456789', $env); + $this->assertStringContainsString('MAIL_FROM_ADDRESS=panel@test.com', $env); + } + + /** + * Test that the Resend driver shows an error when no key is provided. + */ + public function test_resend_driver_warns_without_key(): void + { + $this->artisan('p:environment:mail', [ + '--driver' => 'resend', + '--resend-key' => '', + '--email' => 'panel@test.com', + '--from' => 'Test Panel', + ])->assertExitCode(0); + + // The command should still write the env (it warns but doesn't abort) + $env = file_get_contents(base_path('.env')); + $this->assertStringContainsString('MAIL_DRIVER=resend', $env); + } + + /** + * Test that the mail config includes the resend mailer. + */ + public function test_resend_mailer_is_configured(): void + { + $mailers = config('mail.mailers'); + + $this->assertArrayHasKey('resend', $mailers); + $this->assertEquals('resend', $mailers['resend']['transport']); + } + + /** + * Test that the services config includes the resend key. + */ + public function test_resend_service_config_exists(): void + { + $this->assertNotNull(config('services.resend')); + $this->assertArrayHasKey('key', config('services.resend')); + } + + /** + * Test that existing SMTP configuration is not affected. + */ + public function test_smtp_config_unchanged(): void + { + $smtp = config('mail.mailers.smtp'); + + $this->assertEquals('smtp', $smtp['transport']); + $this->assertArrayHasKey('host', $smtp); + $this->assertArrayHasKey('port', $smtp); + $this->assertArrayHasKey('encryption', $smtp); + $this->assertArrayHasKey('username', $smtp); + $this->assertArrayHasKey('password', $smtp); + } + + /** + * Test that the from address validation works. + */ + public function test_empty_from_address_returns_error(): void + { + $this->artisan('p:environment:mail', [ + '--driver' => 'smtp', + '--host' => 'smtp.test.com', + '--port' => '587', + '--username' => 'user@test.com', + '--password' => 'secret', + '--encryption' => 'tls', + '--email' => '', + '--from' => 'Test Panel', + ])->assertExitCode(1); + } +}