diff --git a/.gitignore b/.gitignore index f11b2a6..b730e63 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ vendor/ composer.lock .phpunit.result.cache .phpunit.cache +test + diff --git a/README.md b/README.md index 0dc8cdf..120cc93 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,41 @@ -

- -

Build Status Latest Stable Version License

-# Package Documentation - -This package allows you to store and send e-mails using a database. +# Introduction -## Contribution - -The package is MIT licenced, meaning it's open source and you are free to copy or fork it and modify it any way you wish. - -We feel the package is currently feature complete, but feel free to send a pull request or help improve existing code. +This package allows you to store and send e-mails using the database. # Requirements -This package requires Laravel 6.0 or higher. - -Please check the [Laravel support policy](https://laravel.com/docs/master/releases#support-policy) table for supported Laravel and PHP versions. +This package requires Laravel 10 or 11. # Installation Require the package using composer. -```bash +```shell composer require stackkit/laravel-database-emails ``` Publish the configuration files. -```bash -php artisan vendor:publish --tag=laravel-database-emails-config +```shell +php artisan vendor:publish --tag=database-emails-config +php artisan vendor:publish --tag=database-emails-migrations ``` Create the database table required for this package. -```bash +```shell php artisan migrate ``` Add the e-mail cronjob to your scheduler ```php -command('email:send')->everyMinute()->withoutOverlapping(5); @@ -65,68 +47,73 @@ protected function schedule(Schedule $schedule) ### Send an email -```php -label('welcome') - ->recipient('john@doe.com') - ->subject('This is a test') - ->view('emails.welcome') - ->variables([ - 'name' => 'John Doe', + ->content(fn (Content $content) => $content + ->view('tests::dummy') + ->with(['name' => 'John Doe']) + ) + ->envelope(fn (Envelope $envelope) => $envelope + ->subject('Hello') + ->from('johndoe@example.com', 'John Doe') + ->to('janedoe@example.com', 'Jane Doe') + ) + ->attachments([ + Attachment::fromStorageDisk('s3', '/invoices/john-doe/march-2024.pdf'), ]) ->send(); +]) ``` -### Specify multiple recipients +### Sending emails to users in your application ```php -recipient([ - 'john@doe.com', - 'jane@doe.com' - ]); + ->user($user) + ->send(); ``` -### CC and BCC +By default, the `name` column will be used to set the recipient's name. If you wish to use something different, you should implement the `preferredEmailName` method in your model. ```php -cc('john@doe.com') - ->cc(['john@doe.com', 'jane@doe.com']) - ->bcc('john@doe.com') - ->bcc(['john@doe.com', 'jane@doe.com']); +class User extends Model +{ + public function preferredEmailName(): string + { + return $this->first_name; + } +} ``` -### Reply-To +By default, the `email` column will be used to set the recipient's e-mail address. If you wish to use something different, you should implement the `preferredEmailAddress` method in your model. ```php -replyTo(['john@doe.com', 'jane@doe.com']); +class User extends Model +{ + public function preferredEmailAddress(): string + { + return $this->work_email; + } +} +``` -Email::compose() - ->replyTo(new Address('john@doe.com', 'John Doe')); +By default, the app locale will be used. If you wish to use something different, you should implement the `preferredEmailLocale` method in your model. -Email::compose() - ->replyTo([ - new Address('john@doe.com', 'John Doe'), - new Address('jane@doe.com', 'Jane Doe'), - ]); +```php +class User extends Model implements HasLocalePreference +{ + public function preferredLocale(): string + { + return $this->locale; + } +} ``` ### Using mailables @@ -134,10 +121,6 @@ Email::compose() You may also pass a mailable to the e-mail composer. ```php -mailable(new OrderShipped()) ->send(); @@ -145,35 +128,30 @@ Email::compose() ### Attachments -```php -attach('/path/to/file'); -``` +To start attaching files to your e-mails, you may use the `attachments` method like you normally would in Laravel. +However, you will have to use this package's `Attachment` class. -Or for in-memory attachments: ```php -attachData('

Your order has shipped!

', 'order.html'); + ->attachments([ + Attachment::fromPath(__DIR__.'/files/pdf-sample.pdf'), + Attachment::fromPath(__DIR__.'/files/my-file.txt')->as('Test123 file'), + Attachment::fromStorageDisk('my-custom-disk', 'test.txt'), + ]) + ->send(); ``` -### Custom Sender +> [!NOTE] +> `Attachment::fromData()` and `Attachment::fromStorage()` are not supported as they work with raw data. -```php -from('john@doe.com', 'John Doe'); + ->model(User::find(1)); ``` ### Scheduling @@ -181,76 +159,75 @@ Email::compose() You may schedule an e-mail by calling `later` instead of `send`. You must provide a Carbon instance or a strtotime valid date. ```php -later('+2 hours'); ``` -### Encryption (Optional) +### Queueing e-mails + +> [!IMPORTANT] +> When queueing mail using the `queue` function, it is no longer necessary to schedule the `email:send` command. -If you wish to encrypt your e-mails, please enable the `encrypt` option in the configuration file. This is disabled by default. Encryption and decryption will be handled by Laravel's built-in encryption mechanism. Please note that by encrypting the e-mail it takes more disk space. +```php +Email::compose()->queue(); -```text -Without encryption +// On a specific connection +Email::compose()->queue(connection: 'sqs'); -7 bytes (label) -16 bytes (recipient) -20 bytes (subject) -48 bytes (view name) -116 bytes (variables) -1874 bytes (e-mail content) -4 bytes (attempts, sending, failed, encrypted) -57 bytes (created_at, updated_at, deleted_at) -... x 10.000 rows = ± 21.55 MB +// On a specific queue +Email::compose()->queue(queue: 'email-queue'); -With encryption the table size is ± 50.58 MB. +// Delay (send mail in 10 minutes) +Email::compose()->queue(delay: now()->addMinutes(10)); ``` +If you need more flexibility, you may also pass your own job class: -### Queueing e-mails +```php +Email::compose()->queue(jobClass: CustomSendEmailJob::class); +``` -**Important**: When queueing mail using the `queue` function, it is no longer necessary to schedule the `email:send` command. Please make sure it is removed from `app/Console/Kernel.php`. +It could look like this: ```php queue(); - -// on a specific connection -Email::compose() - ->queue('sqs'); +namespace App\Jobs; -// on a specific queue -Email::compose() - ->queue(null, 'email-queue'); +use Illuminate\Contracts\Queue\ShouldQueue; +use Stackkit\LaravelDatabaseEmails\SendEmailJob; -// timeout (send mail in 10 minutes) -Email::compose() - ->queue(null, null, now()->addMinutes(10)); +class CustomSendEmailJob extends SendEmailJob implements ShouldQueue +{ + // Define custom retries, backoff, etc... +} ``` -### Test mode (Optional) +### Test mode When enabled, all newly created e-mails will be sent to the specified test e-mail address. This is turned off by default. +```dotenv +DB_EMAILS_TESTING_ENABLED=true +DB_EMAILS_TESTING_EMAIL=your-test-recipient@example.com +``` + ### E-mails to send per minute -To configure how many e-mails should be sent each command, please check the `limit` option. The default is `20` e-mails every command. +To configure how many e-mails should be sent each command. -### Send e-mails immediately (Optional) +```dotenv +DB_EMAILS_LIMIT=20 +``` + +### Send e-mails immediately Useful during development when Laravel Scheduler is not running To enable, set the following environment variable: -``` -LARAVEL_DATABASE_EMAILS_SEND_IMMEDIATELY=true +```dotenv +DB_EMAILS_IMMEDIATELY=true ``` ### Pruning models @@ -260,7 +237,7 @@ use Stackkit\LaravelDatabaseEmails\Email; $schedule->command('model:prune', [ '--model' => [Email::class], -])->everyMinute(); +])->daily(); ``` By default, e-mails are pruned when they are older than 6 months. diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..b38359d --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,80 @@ +# From 6.x to 7.x + +7.x is a bigger change which cleans up parts of the code base and modernizes the package. That means there are a few high-impact changes. + +## Database changes (Impact: High) + +The way addresses are stored in the database has changed. Therefore, emails created in 6.x and below are incompatible. + +When you upgrade, the existing database table will be renamed to "emails_old" and a new table will be created. + +The table migration now needs to be published first. Please run this command: + +```shell +php artisan vendor:publish --tag=database-emails-migrations +``` + +Then, run the migration: + +```shell +php artisan migrate +``` + +## Environment variables, configurations (Impact: High) + +Environment variable names, as well as the config file name, have been shortened. + +Please publish the new configuration file: + +```shell +php artisan vendor:publish --tag=database-emails-config +``` + +You can remove the old configuration file. + +Rename the following environments: + +- `LARAVEL_DATABASE_EMAILS_TESTING_ENABLED` → `DB_EMAILS_TESTING_ENABLED` +- `LARAVEL_DATABASE_EMAILS_SEND_IMMEDIATELY` → `DB_EMAILS_SEND_IMMEDIATELY` + +The following environments are new: + +- `DB_EMAILS_ATTEMPTS` +- `DB_EMAILS_TESTING_EMAIL` +- `DB_EMAILS_LIMIT` +- `DB_EMAILS_IMMEDIATELY` + +The following environments have been removed: + +- `LARAVEL_DATABASE_EMAILS_MANUAL_MIGRATIONS` because migrations are always published. + +## Creating emails (Impact: High) + +The way emails are composed has changed and now borrows a lot from Laravel's mailable. + +```php +use Illuminate\Mail\Mailables\Content; +use Stackkit\LaravelDatabaseEmails\Attachment; +use Stackkit\LaravelDatabaseEmails\Email; +use Illuminate\Mail\Mailables\Envelope; + +Email::compose() + ->content(fn (Content $content) => $content + ->view('tests::dummy') + ->with(['name' => 'John Doe']) + ) + ->envelope(fn (Envelope $envelope) => $envelope + ->subject('Hello') + ->from('johndoe@example.com', 'John Doe') + ->to('janedoe@example.com', 'Jane Doe') + ) + ->attachments([ + Attachment::fromStorageDisk('s3', '/invoices/john-doe/march-2024.pdf'), + ]) + ->send(); +]) +``` + +## Encryption (Impact: moderate/low) + +E-mail encryption has been removed from the package. diff --git a/composer.json b/composer.json index ed7846f..0d16c4d 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,10 @@ }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/" + "Tests\\": "tests/", + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/", + "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" } }, "extra": { @@ -27,24 +30,42 @@ }, "require": { "ext-json": "*", - "laravel/framework": "^10.0|^11.0" + "laravel/framework": "^10.0|^11.0", + "doctrine/dbal": "^3.8" }, "require-dev": { "mockery/mockery": "^1.2", "orchestra/testbench": "^8.0|^9.0", + "nunomaduro/collision": "^7.0|^8.0", "laravel/pint": "^1.14" }, "minimum-stability": "dev", "prefer-stable": true, "scripts": { "l11": [ - "composer update laravel/framework:11.* orchestra/testbench:9.* --with-all-dependencies" + "composer update laravel/framework:11.* orchestra/testbench:9.* nunomaduro/collision:8.* --with-all-dependencies" ], "l10": [ - "composer update laravel/framework:10.* orchestra/testbench:8.* --with-all-dependencies" + "composer update laravel/framework:10.* orchestra/testbench:8.* nunomaduro/collision:7.* --with-all-dependencies" ], "test": [ - "CI_DB_DRIVER=sqlite CI_DB_DATABASE=:memory: phpunit" + "testbench package:test" + ], + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "build": "@php vendor/bin/testbench workbench:build --ansi", + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@build", + "@php vendor/bin/testbench serve" + ], + "lint": [ + "@php vendor/bin/pint", + "@php vendor/bin/phpstan analyse" ] } } diff --git a/config/laravel-database-emails.php b/config/database-emails.php similarity index 60% rename from config/laravel-database-emails.php rename to config/database-emails.php index 6c179ef..0d85003 100644 --- a/config/laravel-database-emails.php +++ b/config/database-emails.php @@ -13,19 +13,7 @@ | */ - 'attempts' => 3, - - /* - |-------------------------------------------------------------------------- - | Encryption - |-------------------------------------------------------------------------- - | - | Here you may enable encryption for all e-mails. The e-mail will be encrypted according - | your application's configuration (OpenSSL AES-256-CBC by default). - | - */ - - 'encrypt' => false, + 'attempts' => env('DB_EMAILS_ATTEMPTS', 3), /* |-------------------------------------------------------------------------- @@ -40,9 +28,9 @@ 'testing' => [ - 'email' => 'test@email.com', + 'email' => env('DB_EMAILS_TESTING_EMAIL'), - 'enabled' => env('LARAVEL_DATABASE_EMAILS_TESTING_ENABLED', true), + 'enabled' => env('DB_EMAILS_TESTING_ENABLED', false), ], @@ -57,7 +45,7 @@ | */ - 'limit' => 20, + 'limit' => env('DB_EMAILS_LIMIT', 20), /* |-------------------------------------------------------------------------- @@ -70,18 +58,5 @@ | */ - 'immediately' => env('LARAVEL_DATABASE_EMAILS_SEND_IMMEDIATELY', false), - - /* - |-------------------------------------------------------------------------- - | Manual migrations - |-------------------------------------------------------------------------- - | - | This option allows you to use: - | `php artisan vendor:publish --tag=laravel-database-emails-migrations` to push migrations - | to your app's folder, so you're free to modify before migrating. - | - */ - - 'manual_migrations' => (bool) env('LARAVEL_DATABASE_EMAILS_MANUAL_MIGRATIONS', false), + 'immediately' => env('DB_EMAILS_IMMEDIATELY', false), ]; diff --git a/database/migrations/2017_12_14_151421_add_attachments_to_emails_table.php b/database/migrations/2017_12_14_151421_add_attachments_to_emails_table.php deleted file mode 100644 index dd49ce6..0000000 --- a/database/migrations/2017_12_14_151421_add_attachments_to_emails_table.php +++ /dev/null @@ -1,34 +0,0 @@ -binary('attachments')->after('body')->nullable(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - // - } -} diff --git a/database/migrations/2017_12_22_114011_add_from_to_emails_table.php b/database/migrations/2017_12_22_114011_add_from_to_emails_table.php deleted file mode 100644 index de25f07..0000000 --- a/database/migrations/2017_12_22_114011_add_from_to_emails_table.php +++ /dev/null @@ -1,34 +0,0 @@ -binary('from')->after('body')->nullable(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - // - } -} diff --git a/database/migrations/2021_12_02_21000_add_queued_at_to_emails_table.php b/database/migrations/2021_12_02_21000_add_queued_at_to_emails_table.php deleted file mode 100644 index 080bf35..0000000 --- a/database/migrations/2021_12_02_21000_add_queued_at_to_emails_table.php +++ /dev/null @@ -1,34 +0,0 @@ -timestamp('queued_at')->after('encrypted')->nullable(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - // - } -} diff --git a/database/migrations/2023_12_28_140000_add_reply_to_to_emails_table.php b/database/migrations/2023_12_28_140000_add_reply_to_to_emails_table.php deleted file mode 100644 index 09d4081..0000000 --- a/database/migrations/2023_12_28_140000_add_reply_to_to_emails_table.php +++ /dev/null @@ -1,30 +0,0 @@ -binary('reply_to')->nullable()->after('bcc'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - // - } -} diff --git a/database/migrations/2017_12_14_151403_create_emails_table.php b/database/migrations/2024_03_16_151608_create_new_table.php similarity index 58% rename from database/migrations/2017_12_14_151403_create_emails_table.php rename to database/migrations/2024_03_16_151608_create_new_table.php index dc49d1f..5c0424a 100644 --- a/database/migrations/2017_12_14_151403_create_emails_table.php +++ b/database/migrations/2024_03_16_151608_create_new_table.php @@ -1,10 +1,10 @@ increments('id'); $table->string('label')->nullable(); - $table->binary('recipient'); - $table->binary('cc')->nullable(); - $table->binary('bcc')->nullable(); - $table->binary('subject'); - $table->string('view', 255); - $table->binary('variables')->nullable(); - $table->binary('body'); + $table->json('recipient'); + $table->json('cc')->nullable(); + $table->json('bcc')->nullable(); + $table->string('subject'); + $table->string('view'); + $table->json('variables')->nullable(); + $table->text('body'); $table->integer('attempts')->default(0); $table->boolean('sending')->default(0); $table->boolean('failed')->default(0); $table->text('error')->nullable(); - $table->boolean('encrypted')->default(0); + $table->json('attachments')->nullable(); + $table->json('from')->nullable(); + $table->nullableMorphs('model'); + $table->json('reply_to')->nullable(); + $table->timestamp('queued_at')->nullable(); $table->timestamp('scheduled_at')->nullable(); - $table->timestamp('sent_at')->nullable(); + $table->timestamp('sent_at')->nullable()->index(); $table->timestamp('delivered_at')->nullable(); $table->timestamps(); $table->softDeletes(); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..972092b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + mysql: + image: mysql:8 + ports: + - '${MYSQL_PORT:-3307}:3306' + environment: + MYSQL_USER: 'test' + MYSQL_PASSWORD: 'test' + MYSQL_DATABASE: 'test' + MYSQL_RANDOM_ROOT_PASSWORD: true + pgsql: + image: postgres:14 + ports: + - '${POSTGRES_PORT:-5432}:5432' + environment: + POSTGRES_USER: 'test' + POSTGRES_PASSWORD: 'test' + POSTGRES_DB: 'test' diff --git a/logo.png b/logo.png deleted file mode 100644 index 69d1e49..0000000 Binary files a/logo.png and /dev/null differ diff --git a/phpunit.xml b/phpunit.xml index eb3301d..a1ec749 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,10 +1,5 @@ - - - - src/ - - + ./tests/ @@ -19,4 +14,9 @@ + + + src/ + + diff --git a/src/Attachment.php b/src/Attachment.php new file mode 100644 index 0000000..dc4d016 --- /dev/null +++ b/src/Attachment.php @@ -0,0 +1,63 @@ +as = $name; + + return $this; + } + + public function withMime(string $mime): self + { + $this->mime = $mime; + + return $this; + } + + public function toArray(): array + { + return [ + 'path' => $this->path, + 'disk' => $this->disk, + 'as' => $this->as, + 'mime' => $this->mime, + ]; + } +} diff --git a/src/Config.php b/src/Config.php index 28640c4..689eff4 100644 --- a/src/Config.php +++ b/src/Config.php @@ -8,61 +8,41 @@ class Config { /** * Get the maximum number of times an e-mail may be attempted to be sent. - * - * @return int */ public static function maxAttemptCount(): int { - return max(config('laravel-database-emails.attempts', 1), 3); - } - - /** - * Determine if newly created e-mails should be encrypted. - * - * @return bool - */ - public static function encryptEmails(): bool - { - return config('laravel-database-emails.encrypt', false); + return max(config('database-emails.attempts', 1), 3); } /** * Determine if newly created e-mails should be sent to the test e-mail address. - * - * @return bool */ public static function testing(): bool { - return (bool) config('laravel-database-emails.testing.enabled', false); + return (bool) config('database-emails.testing.enabled', false); } /** * Get the test e-mail address. - * - * @return string */ public static function testEmailAddress(): string { - return config('laravel-database-emails.testing.email'); + return config('database-emails.testing.email'); } /** * Get the number of e-mails the cronjob may send at a time. - * - * @return int */ public static function cronjobEmailLimit(): int { - return config('laravel-database-emails.limit', 20); + return config('database-emails.limit', 20); } /** * Determine if e-mails should be sent immediately. - * - * @return bool */ public static function sendImmediately(): bool { - return (bool) config('laravel-database-emails.immediately', false); + return (bool) config('database-emails.immediately', false); } } diff --git a/src/Email.php b/src/Email.php index f24831f..5053ca7 100644 --- a/src/Email.php +++ b/src/Email.php @@ -4,573 +4,110 @@ namespace Stackkit\LaravelDatabaseEmails; -use Closure; -use Exception; use Carbon\Carbon; +use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\MassPrunable; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Prunable; +use Illuminate\Database\Eloquent\Relations\MorphTo; +use Throwable; /** - * @property $id - * @property $label - * @property $recipient - * @property $from - * @property $cc - * @property $bcc - * @property $reply_to - * @property $subject - * @property $view - * @property $variables - * @property $body - * @property $attachments - * @property $attempts - * @property $sending - * @property $failed - * @property $error - * @property $encrypted - * @property $queued_at - * @property $scheduled_at - * @property $sent_at - * @property $delivered_at + * @property int $id + * @property string $label + * @property array $recipient + * @property array $from + * @property array $cc + * @property array $bcc + * @property array $reply_to + * @property string $subject + * @property string $view + * @property array $variables + * @property string $body + * @property array $attachments + * @property int $attempts + * @property int $sending + * @property int $failed + * @property int $error + * @property ?Carbon $queued_at + * @property ?Carbon $scheduled_at + * @property ?Carbon $sent_at + * @property ?Carbon $delivered_at */ class Email extends Model { - use HasEncryptedAttributes; - use Prunable; + use MassPrunable; + + protected $casts = [ + 'failed' => 'boolean', + 'recipient' => 'json', + 'from' => 'json', + 'cc' => 'json', + 'bcc' => 'json', + 'reply_to' => 'json', + 'variables' => 'json', + 'attachments' => 'json', + ]; - /** - * The table in which the e-mails are stored. - * - * @var string - */ protected $table = 'emails'; - /** - * The guarded fields. - * - * @var array - */ protected $guarded = []; public static ?Closure $pruneQuery = null; - /** - * Compose a new e-mail. - * - * @return EmailComposer - */ public static function compose(): EmailComposer { return new EmailComposer(new static()); } - /** - * Get the e-mail id. - * - * @return int - */ - public function getId(): int - { - return $this->id; - } - - /** - * Get the e-mail label. - * - * @return string|null - */ - public function getLabel(): ?string - { - return $this->label; - } - - /** - * Get the e-mail recipient. - * - * @return string|array - */ - public function getRecipient() - { - return $this->recipient; - } - - /** - * Get the e-mail recipient. - * - * @return string|array - */ - public function getRecipientAttribute() - { - return $this->recipient; - } - - /** - * Get the e-mail from. - * - * @return array|null - */ - public function getFrom(): ?array - { - return $this->from; - } - - /** - * Get the e-mail from. - * - * @return array|null - */ - public function getFromAttribute(): ?array - { - return $this->from; - } - - /** - * Get the e-mail from address. - * - * @return string|null - */ - public function getFromAddress(): ?string - { - return $this->from['address'] ?? config('mail.from.address'); - } - - /** - * Get the e-mail from address. - * - * @return string|null - */ - public function getFromName(): ?string - { - return $this->from['name'] ?? config('mail.from.name'); - } - - /** - * Get the e-mail recipient(s) as string. - * - * @return string - */ - public function getRecipientsAsString(): string - { - $glue = ', '; - - return implode($glue, (array) $this->recipient); - } - - /** - * Get the e-mail CC addresses. - * - * @return array|string - */ - public function getCc() - { - return $this->cc; - } - - /** - * Get the e-mail CC addresses. - * - * @return array|string - */ - public function getCcAttribute() - { - return $this->cc; - } - - /** - * Get the e-mail BCC addresses. - * - * @return array|string - */ - public function getBcc() - { - return $this->bcc; - } - - /** - * Get the e-mail BCC addresses. - * - * @return array|string - */ - public function getBccAttribute() - { - return $this->bcc; - } - - /** - * Get the e-mail reply-to addresses. - * - * @return array|string - */ - public function getReplyTo() - { - return $this->reply_to; - } - - /** - * Get the e-mail reply-to addresses. - * - * @return array|string - */ - public function getReplyToAttribute() - { - return $this->reply_to; - } - - /** - * Get the e-mail subject. - * - * @return string - */ - public function getSubject(): string - { - return $this->subject; - } - - /** - * Get the e-mail subject. - * - * @return string - */ - public function getSubjectAttribute(): string - { - return $this->subject; - } - - /** - * Get the e-mail view. - * - * @return string - */ - public function getView(): string - { - return $this->view; - } - - /** - * Get the e-mail variables. - * - * @return array|null - */ - public function getVariables(): ?array - { - return $this->variables; - } - - /** - * Get the e-mail variables. - * - * @return array|null - */ - public function getVariablesAttribute(): ?array - { - return $this->variables; - } - - /** - * Get the e-mail body. - * - * @return string - */ - public function getBody(): string - { - return $this->body; - } - - /** - * Get the e-mail body. - * - * @return string - */ - public function getBodyAttribute(): string - { - return $this->body; - } - - /** - * Get the e-mail attachments. - * - * @return array - */ - public function getAttachments(): array - { - return $this->attachments; - } - - /** - * Get the number of times this e-mail was attempted to send. - * - * @return int - */ - public function getAttempts(): int - { - return $this->attempts; - } - - /** - * Get the queued date. - * - * @return string|null - */ - public function getQueuedDate(): ?string - { - return $this->queued_at; - } - - /** - * Get the queued date as a Carbon instance. - * - * @return Carbon - */ - public function getQueuedDateAsCarbon(): Carbon - { - if ($this->queued_at instanceof Carbon) { - return $this->queued_at; - } - - return Carbon::parse($this->queued_at); - } - - /** - * Get the scheduled date. - * - * @return string|null - */ - public function getScheduledDate(): ?string - { - return $this->scheduled_at; - } - - /** - * Determine if the e-mail has variables defined. - * - * @return bool - */ - public function hasVariables(): bool - { - return ! is_null($this->variables); - } - - /** - * Get the scheduled date as a Carbon instance. - * - * @return Carbon - */ - public function getScheduledDateAsCarbon(): Carbon - { - if ($this->scheduled_at instanceof Carbon) { - return $this->scheduled_at; - } - - return Carbon::parse($this->scheduled_at); - } - - /** - * Get the send date for this e-mail. - * - * @return string|null - */ - public function getSendDate(): ?string - { - return $this->sent_at; - } - - /** - * Get the send error. - * - * @return string|string - */ - public function getError(): ?string - { - return $this->error; - } - - /** - * Determine if the e-mail should be sent with custom from values. - * - * @return bool - */ - public function hasFrom(): bool - { - return is_array($this->from) && count($this->from) > 0; - } - - /** - * Determine if the e-mail should be sent as a carbon copy. - * - * @return bool - */ - public function hasCc(): bool - { - return strlen($this->getRawDatabaseValue('cc')) > 0; - } - - /** - * Determine if the e-mail should be sent as a blind carbon copy. - * - * @return bool - */ - public function hasBcc(): bool - { - return strlen($this->getRawDatabaseValue('bcc')) > 0; - } - - /** - * Determine if the e-mail should sent with reply-to. - * - * @return bool - */ - public function hasReplyTo(): bool - { - return strlen($this->getRawDatabaseValue('reply_to') ?: '') > 0; - } - - /** - * Determine if the e-mail is scheduled to be sent later. - * - * @return bool - */ - public function isScheduled(): bool - { - return ! is_null($this->getScheduledDate()); - } - - /** - * Determine if the e-mail is encrypted. - * - * @return bool - */ - public function isEncrypted(): bool - { - return (bool) $this->getRawDatabaseValue('encrypted'); - } - - /** - * Determine if the e-mail is sent. - * - * @return bool - */ public function isSent(): bool { return ! is_null($this->sent_at); } - /** - * Determine if the e-mail failed to be sent. - * - * @return bool - */ public function hasFailed(): bool { return $this->failed == 1; } - /** - * Mark the e-mail as sending. - * - * @return void - */ public function markAsSending(): void { $this->update([ 'attempts' => $this->attempts + 1, - 'sending' => 1, + 'sending' => 1, ]); } - /** - * Mark the e-mail as sent. - * - * @return void - */ public function markAsSent(): void { - $now = Carbon::now()->toDateTimeString(); - $this->update([ 'sending' => 0, - 'sent_at' => $now, - 'failed' => 0, - 'error' => '', + 'sent_at' => now(), + 'failed' => 0, + 'error' => '', ]); } - /** - * Mark the e-mail as failed. - * - * @param Exception $exception - * @return void - */ - public function markAsFailed(Exception $exception): void + public function markAsFailed(Throwable $exception): void { $this->update([ 'sending' => 0, - 'failed' => 1, - 'error' => (string) $exception, + 'failed' => 1, + 'error' => (string) $exception, ]); } - /** - * Send the e-mail. - * - * @return void - */ public function send(): void { (new Sender())->send($this); } - /** - * Retry sending the e-mail. - * - * @return void - */ - public function retry(): void - { - $retry = $this->replicate(); - - $retry->fill( - [ - 'id' => null, - 'attempts' => 0, - 'sending' => 0, - 'failed' => 0, - 'error' => null, - 'sent_at' => null, - 'delivered_at' => null, - ] - ); - - $retry->save(); - } - - /** - * @param string $key - * @param mixed $default - * @return mixed - */ - public function getRawDatabaseValue(string $key = null, $default = null) - { - if (method_exists($this, 'getRawOriginal')) { - return $this->getRawOriginal($key, $default); - } - - return $this->getOriginal($key, $default); - } - - /** - * @param Closure $closure - * @return void - */ - public static function pruneWhen(Closure $closure) + public static function pruneWhen(Closure $closure): void { static::$pruneQuery = $closure; } - /** - * @return Builder - */ - public function prunable() + public function prunable(): Builder { if (static::$pruneQuery) { return (static::$pruneQuery)($this); @@ -578,4 +115,9 @@ public function prunable() return $this->where('created_at', '<', now()->subMonths(6)); } + + public function model(): MorphTo + { + return $this->morphTo(); + } } diff --git a/src/EmailComposer.php b/src/EmailComposer.php index 352e63e..fc4d67c 100644 --- a/src/EmailComposer.php +++ b/src/EmailComposer.php @@ -4,308 +4,180 @@ namespace Stackkit\LaravelDatabaseEmails; +use Closure; +use Illuminate\Contracts\Translation\HasLocalePreference; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Foundation\Auth\User; use Illuminate\Mail\Mailable; +use Illuminate\Mail\Mailables\Content; +use Illuminate\Mail\Mailables\Envelope; +use Illuminate\Support\Carbon; class EmailComposer { - /** - * The e-mail that is being composed. - * - * @var Email - */ - private $email; - - /** - * The e-email data. - * - * @var array - */ - protected $data = []; - - /** - * Create a new EmailComposer instance. - * - * @param Email $email - */ - public function __construct(Email $email) - { - $this->email = $email; - } + public ?Mailable $mailable = null; + + public ?Envelope $envelope = null; + + public ?Content $content = null; + + public ?array $attachments = null; + + public ?string $locale = null; + + public ?Model $model = null; + + public ?bool $queued = null; - /** - * Get the e-mail that is being composed. - * - * @return Email - */ - public function getEmail(): Email + public ?string $connection = null; + + public ?string $queue = null; + + public int|Carbon|null $delay = null; + + public ?string $jobClass = null; + + public function __construct(public Email $email) { - return $this->email; + // } - /** - * Set a data value. - * - * @param string $key - * @param mixed $value - * @return self - */ - public function setData(string $key, $value): self + public function envelope(null|Envelope|Closure $envelope = null): self { - $this->data[$key] = $value; + if ($envelope instanceof Closure) { + $this->envelope = $envelope($this->envelope ?: new Envelope()); + + return $this; + } + + $this->envelope = $envelope; return $this; } - /** - * Get a data value. - * - * @param string $key - * @param mixed $default - * @return mixed - */ - public function getData(string $key, $default = null) + public function content(null|Content|Closure $content = null): self { - if (! is_null($default) && ! $this->hasData($key)) { - return $default; + if ($content instanceof Closure) { + $this->content = $content($this->content ?: new Content()); + + return $this; } - return $this->data[$key]; - } + $this->content = $content; - /** - * Determine if the given data value was set. - * - * @param string $key - * @return bool - */ - public function hasData(string $key): bool - { - return isset($this->data[$key]); + return $this; } - /** - * Set the e-mail label. - * - * @param string $label - * @return self - */ - public function label(string $label): self + public function attachments(null|array|Closure $attachments = null): self { - return $this->setData('label', $label); - } + if ($attachments instanceof Closure) { + $this->attachments = $attachments($this->attachments ?: []); - /** - * Set the e-mail from address and aname. - * - * @param string|null $address - * @param string|null $name - * @return self - */ - public function from(?string $address = null, ?string $name = null): self - { - return $this->setData('from', compact('address', 'name')); - } + return $this; + } - /** - * Set the e-mail recipient(s). - * - * @param string|array $recipient - * @return self - */ - public function recipient($recipient): self - { - return $this->setData('recipient', $recipient); - } + $this->attachments = $attachments; - /** - * Define the carbon-copy address(es). - * - * @param string|array $cc - * @return self - */ - public function cc($cc): self - { - return $this->setData('cc', $cc); + return $this; } - /** - * Define the blind carbon-copy address(es). - * - * @param string|array $bcc - * @return self - */ - public function bcc($bcc): self + public function user(User $user) { - return $this->setData('bcc', $bcc); - } + return $this->envelope(function (Envelope $envelope) use ($user) { + $name = method_exists($user, 'preferredEmailName') + ? $user->preferredEmailName() + : ($user->name ?? null); - /** - * Define the reply-to address(es). - * - * @param string|array $replyTo - * @return self - */ - public function replyTo($replyTo): self - { - return $this->setData('reply_to', $replyTo); - } + $email = method_exists($user, 'preferredEmailAddress') + ? $user->preferredEmailAddress() + : $user->email; - /** - * Set the e-mail subject. - * - * @param string $subject - * @return self - */ - public function subject(string $subject): self - { - return $this->setData('subject', $subject); - } + if ($user instanceof HasLocalePreference) { + $this->locale = $user->preferredLocale(); + } - /** - * Set the e-mail view. - * - * @param string $view - * @return self - */ - public function view(string $view): self - { - return $this->setData('view', $view); + return $envelope->to($email, $name); + }); } - /** - * Set the e-mail variables. - * - * @param array $variables - * @return self - */ - public function variables(array $variables): self + public function model(Model $model) { - return $this->setData('variables', $variables); + $this->model = $model; + + return $this; } - /** - * Schedule the e-mail. - * - * @param mixed $scheduledAt - * @return Email - */ - public function schedule($scheduledAt): Email + public function label(string $label): self { - return $this->later($scheduledAt); + $this->email->label = $label; + + return $this; } - /** - * Schedule the e-mail. - * - * @param mixed $scheduledAt - * @return Email - */ public function later($scheduledAt): Email { - $this->setData('scheduled_at', $scheduledAt); + $this->email->scheduled_at = Carbon::parse($scheduledAt); return $this->send(); } - /** - * Queue the e-mail. - * - * @param string|null $connection - * @param string|null $queue - * @param \DateTimeInterface|\DateInterval|int|null $delay - * @return Email - */ - public function queue(?string $connection = null, ?string $queue = null, $delay = null): Email + public function queue(?string $connection = null, ?string $queue = null, $delay = null, ?string $jobClass = null): Email { $connection = $connection ?: config('queue.default'); $queue = $queue ?: 'default'; - $this->setData('queued', true); - $this->setData('connection', $connection); - $this->setData('queue', $queue); - $this->setData('delay', $delay); + $this->email->queued_at = now(); + + $this->queued = true; + $this->connection = $connection; + $this->queue = $queue; + $this->delay = $delay; + $this->jobClass = $jobClass; return $this->send(); } - /** - * Set the Mailable. - * - * @param Mailable $mailable - * @return self - */ public function mailable(Mailable $mailable): self { - $this->setData('mailable', $mailable); + $this->mailable = $mailable; (new MailableReader())->read($this); return $this; } - /** - * Attach a file to the e-mail. - * - * @param string $file - * @param array $options - * @return self - */ - public function attach(string $file, array $options = []): self - { - $attachments = $this->hasData('attachments') ? $this->getData('attachments') : []; - - $attachments[] = compact('file', 'options'); - - return $this->setData('attachments', $attachments); - } - - /** - * Attach in-memory data as an attachment. - * - * @param string $data - * @param string $name - * @param array $options - * @return self - */ - public function attachData(string $data, string $name, array $options = []): self - { - $attachments = $this->hasData('rawAttachments') ? $this->getData('rawAttachments') : []; - - $attachments[] = compact('data', 'name', 'options'); - - return $this->setData('rawAttachments', $attachments); - } - - /** - * Send the e-mail. - * - * @return Email - */ public function send(): Email { - (new Validator())->validate($this); - - (new Preparer())->prepare($this); + if ($this->envelope && $this->content) { + (new MailableReader())->read($this); + } - if (Config::encryptEmails()) { - (new Encrypter())->encrypt($this); + if (! $this->email->from) { + $this->email->from = [ + 'address' => config('mail.from.address'), + 'name' => config('mail.from.name'), + ]; } $this->email->save(); $this->email->refresh(); - if ($this->getData('queued', false) === true) { - dispatch(new SendEmailJob($this->email)) - ->onConnection($this->getData('connection')) - ->onQueue($this->getData('queue')) - ->delay($this->getData('delay')); + if (Config::sendImmediately()) { + $this->email->send(); return $this->email; } - if (Config::sendImmediately()) { - $this->email->send(); + if ($this->queued) { + $job = $this->jobClass ?: SendEmailJob::class; + + dispatch(new $job($this->email)) + ->onConnection($this->connection) + ->onQueue($this->queue) + ->delay($this->delay); + + return $this->email; } return $this->email; diff --git a/src/Encrypter.php b/src/Encrypter.php deleted file mode 100644 index 14052a7..0000000 --- a/src/Encrypter.php +++ /dev/null @@ -1,130 +0,0 @@ -setEncrypted($composer); - - $this->encryptRecipients($composer); - - $this->encryptReplyTo($composer); - - $this->encryptFrom($composer); - - $this->encryptSubject($composer); - - $this->encryptVariables($composer); - - $this->encryptBody($composer); - } - - /** - * Mark the e-mail as encrypted. - * - * @param EmailComposer $composer - */ - private function setEncrypted(EmailComposer $composer): void - { - $composer->getEmail()->setAttribute('encrypted', 1); - } - - /** - * Encrypt the e-mail reply-to. - * - * @param EmailComposer $composer - */ - private function encryptReplyTo(EmailComposer $composer): void - { - $email = $composer->getEmail(); - - $email->fill([ - 'reply_to' => $composer->hasData('reply_to') ? encrypt($email->reply_to) : '', - ]); - } - - /** - * Encrypt the e-mail addresses of the recipients. - * - * @param EmailComposer $composer - */ - private function encryptRecipients(EmailComposer $composer): void - { - $email = $composer->getEmail(); - - $email->fill([ - 'recipient' => encrypt($email->recipient), - 'cc' => $composer->hasData('cc') ? encrypt($email->cc) : '', - 'bcc' => $composer->hasData('bcc') ? encrypt($email->bcc) : '', - ]); - } - - /** - * Encrypt the e-mail addresses for the from field. - * - * @param EmailComposer $composer - */ - private function encryptFrom(EmailComposer $composer): void - { - $email = $composer->getEmail(); - - $email->fill([ - 'from' => encrypt($email->from), - ]); - } - - /** - * Encrypt the e-mail subject. - * - * @param EmailComposer $composer - */ - private function encryptSubject(EmailComposer $composer): void - { - $email = $composer->getEmail(); - - $email->fill([ - 'subject' => encrypt($email->subject), - ]); - } - - /** - * Encrypt the e-mail variables. - * - * @param EmailComposer $composer - */ - private function encryptVariables(EmailComposer $composer): void - { - if (! $composer->hasData('variables')) { - return; - } - - $email = $composer->getEmail(); - - $email->fill([ - 'variables' => encrypt($email->variables), - ]); - } - - /** - * Encrypt the e-mail body. - * - * @param EmailComposer $composer - */ - private function encryptBody(EmailComposer $composer): void - { - $email = $composer->getEmail(); - - $email->fill([ - 'body' => encrypt($email->body), - ]); - } -} diff --git a/src/HasEncryptedAttributes.php b/src/HasEncryptedAttributes.php deleted file mode 100644 index 01dfbb4..0000000 --- a/src/HasEncryptedAttributes.php +++ /dev/null @@ -1,88 +0,0 @@ -attributes[$key]; - - if ($this->isEncrypted() && in_array($key, $this->encrypted)) { - try { - $value = decrypt($value); - } catch (DecryptException $e) { - $value = ''; - } - } - - if (in_array($key, $this->encoded) && is_string($value)) { - $decoded = json_decode($value, true); - - if (! is_null($decoded)) { - $value = $decoded; - } - } - - // BC fix for attachments in 4.1.0 and lower. - // Attachments were stored json encoded. - // Because this doesn't work for raw attachments, value is now serialized. - // Check if value is json encoded or serialized, and decode or unserialize accordingly. - if ($key == 'attachments') { - if (substr($value, 0, 2) === 'a:') { - $unserialized = @unserialize($value); - if ($value !== false) { - $value = $unserialized; - } - } else { - $decoded = json_decode($value, true); - - if (! is_null($decoded)) { - $value = $decoded; - } - } - } - - return $value; - } -} diff --git a/src/LaravelDatabaseEmailsServiceProvider.php b/src/LaravelDatabaseEmailsServiceProvider.php index 57e249b..30a9ea4 100644 --- a/src/LaravelDatabaseEmailsServiceProvider.php +++ b/src/LaravelDatabaseEmailsServiceProvider.php @@ -10,8 +10,6 @@ class LaravelDatabaseEmailsServiceProvider extends ServiceProvider { /** * Perform post-registration booting of services. - * - * @return void */ public function boot(): void { @@ -21,42 +19,32 @@ public function boot(): void /** * Boot the config for the package. - * - * @return void */ private function bootConfig(): void { - $baseDir = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR; - $configDir = $baseDir . 'config' . DIRECTORY_SEPARATOR; + $baseDir = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR; + $configDir = $baseDir.'config'.DIRECTORY_SEPARATOR; $this->publishes([ - $configDir . 'laravel-database-emails.php' => config_path('laravel-database-emails.php'), - ], 'laravel-database-emails-config'); + $configDir.'laravel-database-emails.php' => config_path('laravel-database-emails.php'), + ], 'database-emails-config'); } /** * Boot the database for the package. - * - * @return void */ private function bootDatabase(): void { - $baseDir = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR; - $migrationsDir = $baseDir . 'database' . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR; + $baseDir = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR; + $migrationsDir = $baseDir.'database'.DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR; - if ($this->app['config']->get('laravel-database-emails.manual_migrations')) { - $this->publishes([ - $migrationsDir => "{$this->app->databasePath()}/migrations", - ], 'laravel-database-emails-migrations'); - } else { - $this->loadMigrationsFrom([$migrationsDir]); - } + $this->publishes([ + $migrationsDir => "{$this->app->databasePath()}/migrations", + ], 'database-emails-migrations'); } /** * Register the service provider. - * - * @return void */ public function register(): void { diff --git a/src/MailableReader.php b/src/MailableReader.php index 86be654..bdc3876 100644 --- a/src/MailableReader.php +++ b/src/MailableReader.php @@ -4,28 +4,58 @@ namespace Stackkit\LaravelDatabaseEmails; +use Error; use Exception; -use Illuminate\Container\Container; -use ReflectionObject; +use Illuminate\Mail\Mailable; +use Illuminate\Mail\Mailables\Content; +use Illuminate\Mail\Mailables\Envelope; +use Illuminate\Support\Traits\Localizable; class MailableReader { + use Localizable; + /** * Read the mailable and pass the data to the email composer. - * - * @param EmailComposer $composer */ public function read(EmailComposer $composer): void { - if (method_exists($composer->getData('mailable'), 'prepareMailableForDelivery')) { - $reflected = (new ReflectionObject($composer->getData('mailable'))); - $method = $reflected->getMethod('prepareMailableForDelivery'); - $method->setAccessible(true); - $method->invoke($composer->getData('mailable')); - } else { - Container::getInstance()->call([$composer->getData('mailable'), 'build']); + $canBeSent = $composer->mailable || ($composer->envelope && $composer->content); + + if (! $canBeSent) { + throw new Error('E-mail cannot be sent: no mailable or envelope and content provided.'); + } + + if ($composer->envelope && $composer->content) { + $composer->mailable = new class($composer) extends Mailable + { + public function __construct(private EmailComposer $composer) + { + // + } + + public function content(): Content + { + return $this->composer->content; + } + + public function envelope(): Envelope + { + return $this->composer->envelope; + } + + public function attachments(): array + { + return $this->composer->attachments ?? []; + } + }; } + (fn (Mailable $mailable) => $mailable->prepareMailableForDelivery())->call( + $composer->mailable, + $composer->mailable, + ); + $this->readRecipient($composer); $this->readFrom($composer); @@ -41,136 +71,130 @@ public function read(EmailComposer $composer): void $this->readBody($composer); $this->readAttachments($composer); + + $this->readModel($composer); } /** * Convert the mailable addresses array into a array with only e-mails. * - * @param string $from - * @return array + * @param string $from */ private function convertMailableAddresses($from): array { - return collect($from)->map(function ($recipient) { - return $recipient['address']; + return collect($from)->mapWithKeys(function ($recipient) { + return [$recipient['address'] => $recipient['name']]; })->toArray(); } /** * Read the mailable recipient to the email composer. - * - * @param EmailComposer $composer */ private function readRecipient(EmailComposer $composer): void { - $to = $this->convertMailableAddresses( - $composer->getData('mailable')->to - ); + if (config('database-emails.testing.enabled')) { + $composer->email->recipient = [ + config('database-emails.testing.email') => null, + ]; - $composer->recipient($to); + return; + } + + $composer->email->recipient = $this->prepareAddressForDatabaseStorage( + $composer->mailable->to); } /** * Read the mailable from field to the email composer. - * - * @param EmailComposer $composer */ private function readFrom(EmailComposer $composer): void { - $from = reset($composer->getData('mailable')->from); - - if (!$from) { - return; - } - - $composer->from( - $from['address'], - $from['name'] - ); + $composer->email->from = head($composer->mailable->from); } /** * Read the mailable cc to the email composer. - * - * @param EmailComposer $composer */ private function readCc(EmailComposer $composer): void { - $cc = $this->convertMailableAddresses( - $composer->getData('mailable')->cc - ); - - $composer->cc($cc); + $composer->email->cc = $this->prepareAddressForDatabaseStorage( + $composer->mailable->cc); } /** * Read the mailable bcc to the email composer. - * - * @param EmailComposer $composer */ private function readBcc(EmailComposer $composer): void { - $bcc = $this->convertMailableAddresses( - $composer->getData('mailable')->bcc - ); - - $composer->bcc($bcc); + $composer->email->bcc = $this->prepareAddressForDatabaseStorage( + $composer->mailable->bcc); } /** * Read the mailable reply-to to the email composer. - * - * @param EmailComposer $composer */ private function readReplyTo(EmailComposer $composer): void { - $replyTo = $this->convertMailableAddresses( - $composer->getData('mailable')->replyTo - ); - - $composer->replyTo($replyTo); + $composer->email->reply_to = $this->prepareAddressForDatabaseStorage( + $composer->mailable->replyTo); } /** * Read the mailable subject to the email composer. - * - * @param EmailComposer $composer */ private function readSubject(EmailComposer $composer): void { - $composer->subject($composer->getData('mailable')->subject); + $composer->email->subject = $composer->mailable->subject; } /** * Read the mailable body to the email composer. * - * @param EmailComposer $composer * @throws Exception */ private function readBody(EmailComposer $composer): void { - $composer->setData('view', ''); + /** @var Mailable $mailable */ + $mailable = $composer->mailable; + + $composer->email->view = $mailable->view; + $composer->email->variables = $mailable->buildViewData(); - $mailable = $composer->getData('mailable'); + $localeToUse = $composer->locale ?? app()->currentLocale(); - $composer->setData('body', view($mailable->view, $mailable->buildViewData())->render()); + $this->withLocale( + $localeToUse, + fn () => $composer->email->body = view($mailable->view, $mailable->buildViewData())->render(), + ); } /** * Read the mailable attachments to the email composer. - * - * @param EmailComposer $composer */ private function readAttachments(EmailComposer $composer): void { - $mailable = $composer->getData('mailable'); + $mailable = $composer->mailable; - foreach ((array) $mailable->attachments as $attachment) { - call_user_func_array([$composer, 'attach'], $attachment); - } + $composer->email->attachments = array_map(function (array $attachment) { + if (! $attachment['file'] instanceof Attachment) { + throw new Error('The attachment is not an instance of '.Attachment::class.'.'); + } + + return $attachment['file']->toArray(); + }, $mailable->attachments); + } - foreach ((array) $mailable->rawAttachments as $rawAttachment) { - call_user_func_array([$composer, 'attachData'], $rawAttachment); + public function readModel(EmailComposer $composer): void + { + if ($composer->model) { + $composer->email->model()->associate($composer->model); } } + + private function prepareAddressForDatabaseStorage(array $addresses): array + { + return collect($addresses)->mapWithKeys(function ($recipient) { + return [$recipient['address'] => $recipient['name']]; + })->toArray(); + } } diff --git a/src/Preparer.php b/src/Preparer.php deleted file mode 100644 index ea405d0..0000000 --- a/src/Preparer.php +++ /dev/null @@ -1,287 +0,0 @@ -prepareLabel($composer); - - $this->prepareRecipient($composer); - - $this->prepareFrom($composer); - - $this->prepareCc($composer); - - $this->prepareBcc($composer); - - $this->prepareReplyTo($composer); - - $this->prepareSubject($composer); - - $this->prepareView($composer); - - $this->prepareVariables($composer); - - $this->prepareBody($composer); - - $this->prepareAttachments($composer); - - $this->prepareScheduled($composer); - - $this->prepareImmediately($composer); - - $this->prepareQueued($composer); - } - - /** - * Prepare the label for database storage. - * - * @param EmailComposer $composer - */ - private function prepareLabel(EmailComposer $composer): void - { - if (! $composer->hasData('label')) { - return; - } - - $composer->getEmail()->fill([ - 'label' => $composer->getData('label'), - ]); - } - - /** - * Prepare the recipient for database storage. - * - * @param EmailComposer $composer - */ - private function prepareRecipient(EmailComposer $composer): void - { - if (Config::testing()) { - $composer->recipient(Config::testEmailAddress()); - } - - $composer->getEmail()->fill([ - 'recipient' => json_encode($composer->getData('recipient')), - ]); - } - - /** - * Prepare the from values for database storage. - * - * @param EmailComposer $composer - */ - private function prepareFrom(EmailComposer $composer): void - { - $composer->getEmail()->fill([ - 'from' => json_encode($composer->getData('from', '')), - ]); - } - - /** - * Prepare the carbon copies for database storage. - * - * @param EmailComposer $composer - */ - private function prepareCc(EmailComposer $composer): void - { - if (Config::testing()) { - $composer->setData('cc', []); - } - - $composer->getEmail()->fill([ - 'cc' => json_encode($composer->getData('cc', [])), - ]); - } - - /** - * Prepare the carbon copies for database storage. - * - * @param EmailComposer $composer - */ - private function prepareBcc(EmailComposer $composer): void - { - if (Config::testing()) { - $composer->setData('bcc', []); - } - - $composer->getEmail()->fill([ - 'bcc' => json_encode($composer->getData('bcc', [])), - ]); - } - - /** - * Prepare the reply-to for database storage. - * - * @param EmailComposer $composer - */ - private function prepareReplyTo(EmailComposer $composer): void - { - $value = $composer->getData('reply_to', []); - - if (! is_array($value)) { - $value = [$value]; - } - - foreach ($value as $i => $v) { - if ($v instanceof Address) { - $value[$i] = [ - 'address' => $v->address, - 'name' => $v->name, - ]; - } - } - - $composer->getEmail()->fill([ - 'reply_to' => json_encode($value), - ]); - } - - /** - * Prepare the subject for database storage. - * - * @param EmailComposer $composer - */ - private function prepareSubject(EmailComposer $composer): void - { - $composer->getEmail()->fill([ - 'subject' => $composer->getData('subject'), - ]); - } - - /** - * Prepare the view for database storage. - * - * @param EmailComposer $composer - */ - private function prepareView(EmailComposer $composer): void - { - $composer->getEmail()->fill([ - 'view' => $composer->getData('view'), - ]); - } - - /** - * Prepare the variables for database storage. - * - * @param EmailComposer $composer - */ - private function prepareVariables(EmailComposer $composer): void - { - if (! $composer->hasData('variables')) { - return; - } - - $composer->getEmail()->fill([ - 'variables' => json_encode($composer->getData('variables')), - ]); - } - - /** - * Prepare the e-mail body for database storage. - * - * @param EmailComposer $composer - */ - private function prepareBody(EmailComposer $composer): void - { - // If the body was predefined (by for example a mailable), use that. - if ($composer->hasData('body')) { - $body = $composer->getData('body'); - } else { - $body = view( - $composer->getData('view'), - $composer->hasData('variables') ? $composer->getData('variables') : [] - )->render(); - } - - $composer->getEmail()->fill(compact('body')); - } - - /** - * Prepare the e-mail attachments. - * - * @param EmailComposer $composer - */ - private function prepareAttachments(EmailComposer $composer): void - { - $attachments = []; - - foreach ((array) $composer->getData('attachments', []) as $attachment) { - $attachments[] = [ - 'type' => 'attachment', - 'attachment' => $attachment, - ]; - } - - foreach ((array) $composer->getData('rawAttachments', []) as $rawAttachment) { - $attachments[] = [ - 'type' => 'rawAttachment', - 'attachment' => $rawAttachment, - ]; - } - - $composer->getEmail()->fill([ - 'attachments' => serialize($attachments), - ]); - } - - /** - * Prepare the scheduled date for database storage. - * - * @param EmailComposer $composer - */ - private function prepareScheduled(EmailComposer $composer): void - { - if (! $composer->hasData('scheduled_at')) { - return; - } - - $scheduled = $composer->getData('scheduled_at'); - - if (is_string($scheduled)) { - $scheduled = Carbon::parse($scheduled); - } - - $composer->getEmail()->fill([ - 'scheduled_at' => $scheduled->toDateTimeString(), - ]); - } - - /** - * Prepare the e-mail so it can be sent immediately. - * - * @param EmailComposer $composer - */ - private function prepareImmediately(EmailComposer $composer): void - { - if (Config::sendImmediately()) { - $composer->getEmail()->fill(['sending' => 1]); - } - } - - /** - * Prepare the queued date. - * - * @param EmailComposer $composer - */ - private function prepareQueued(EmailComposer $composer): void - { - if ($composer->getData('queued', false) === true) { - $composer->getEmail()->fill([ - 'queued_at' => Carbon::now()->toDateTimeString(), - ]); - } - } - -} diff --git a/src/SendEmailJob.php b/src/SendEmailJob.php index a7ba528..a865048 100644 --- a/src/SendEmailJob.php +++ b/src/SendEmailJob.php @@ -26,6 +26,6 @@ public function __construct(Email $email) public function handle(): void { - (new Sender())->send($this->email); + $this->email->send(); } } diff --git a/src/SendEmailsCommand.php b/src/SendEmailsCommand.php index 039ca37..71817ee 100644 --- a/src/SendEmailsCommand.php +++ b/src/SendEmailsCommand.php @@ -4,96 +4,42 @@ namespace Stackkit\LaravelDatabaseEmails; -use Exception; use Illuminate\Console\Command; -use Illuminate\Database\Eloquent\Collection; +use Throwable; class SendEmailsCommand extends Command { - /** - * The name and signature of the console command. - * - * @var string - */ protected $signature = 'email:send'; - /** - * The console command description. - * - * @var string - */ protected $description = 'Send all queued e-mails'; - /** - * The e-mail repository. - * - * @var Store - */ - protected $store; - - /** - * Create a new SendEmailsCommand instance. - * - * @param Store $store - */ - public function __construct(Store $store) + public function handle(Store $store): void { - parent::__construct(); - - $this->store = $store; - } - - /** - * Execute the console command. - * - * @return void - */ - public function handle(): void - { - $emails = $this->store->getQueue(); + $emails = $store->getQueue(); if ($emails->isEmpty()) { - $this->line('There is nothing to send.'); + $this->components->info('There is nothing to send.'); return; } - $progress = $this->output->createProgressBar($emails->count()); + $this->components->info('Sending '.count($emails).' e-mail(s).'); foreach ($emails as $email) { - $progress->advance(); + $recipients = implode(', ', array_keys($email->recipient)); + $line = str($email->subject)->limit(40).' - '.str($recipients)->limit(40); - try { + rescue(function () use ($email, $line) { $email->send(); - } catch (Exception $e) { - $email->markAsFailed($e); - } - } - $progress->finish(); - - $this->result($emails); - } - - /** - * Output a table with the cronjob result. - * - * @param Collection $emails - * @return void - */ - protected function result(Collection $emails): void - { - $headers = ['ID', 'Recipient', 'Subject', 'Status']; + $this->components->twoColumnDetail($line, 'DONE'); + }, function (Throwable $e) use ($email, $line) { + $email->markAsFailed($e); - $this->line("\n"); + $this->components->twoColumnDetail($line, 'FAIL'); + }); + } - $this->table($headers, $emails->map(function (Email $email) { - return [ - $email->getId(), - $email->getRecipientsAsString(), - $email->getSubject(), - $email->hasFailed() ? 'Failed' : 'OK', - ]; - })); + $this->newLine(); } } diff --git a/src/Sender.php b/src/Sender.php index 60a80fe..736c830 100644 --- a/src/Sender.php +++ b/src/Sender.php @@ -4,16 +4,12 @@ namespace Stackkit\LaravelDatabaseEmails; +use Illuminate\Mail\Attachment; use Illuminate\Mail\Message; use Illuminate\Support\Facades\Mail; class Sender { - /** - * Send the given e-mail. - * - * @param Email $email - */ public function send(Email $email): void { if ($email->isSent()) { @@ -26,45 +22,39 @@ public function send(Email $email): void $this->buildMessage($message, $email); }); - // This is used so we can assert things on the sent message in Laravel 9+ since we cannot use - // the Swift Mailer plugin anymore. So this is purely used for in the PHPUnit tests. - if (version_compare(app()->version(), '9.0.0', '>=') && !is_null($sentMessage)) { - event(new MessageSent($sentMessage)); - } + event(new MessageSent($sentMessage)); $email->markAsSent(); } - /** - * Build the e-mail message. - * - * @param Message $message - * @param Email $email - */ private function buildMessage(Message $message, Email $email): void { - $message->to($email->getRecipient()) - ->cc($email->hasCc() ? $email->getCc() : []) - ->bcc($email->hasBcc() ? $email->getBcc() : []) - ->replyTo($email->hasReplyTo() ? $email->getReplyTo() : []) - ->subject($email->getSubject()) - ->from($email->getFromAddress(), $email->getFromName()); - - if (version_compare(app()->version(), '9.0.0', '>=')) { - // Symfony Mailer - $message->html($email->getBody()); - } else { - // SwiftMail - $message->setBody($email->getBody(), 'text/html'); - } - - $attachmentMap = [ - 'attachment' => 'attach', - 'rawAttachment' => 'attachData', - ]; - - foreach ($email->getAttachments() as $attachment) { - call_user_func_array([$message, $attachmentMap[$attachment['type']]], $attachment['attachment']); + $message->to($email->recipient) + ->cc($email->cc ?: []) + ->bcc($email->bcc ?: []) + ->replyTo($email->reply_to ?: []) + ->subject($email->subject) + ->from($email->from['address'], $email->from['name']) + ->html($email->body); + + foreach ($email->attachments as $dbAttachment) { + $attachment = match (true) { + isset($dbAttachment['disk']) => Attachment::fromStorageDisk( + $dbAttachment['disk'], + $dbAttachment['path'] + ), + default => Attachment::fromPath($dbAttachment['path']), + }; + + if (! empty($dbAttachment['mime'])) { + $attachment->withMime($dbAttachment['mime']); + } + + if (! empty($dbAttachment['as'])) { + $attachment->as($dbAttachment['as']); + } + + $message->attach($attachment); } } } diff --git a/src/SentMessage.php b/src/SentMessage.php index 570d524..f3dff66 100644 --- a/src/SentMessage.php +++ b/src/SentMessage.php @@ -4,22 +4,30 @@ namespace Stackkit\LaravelDatabaseEmails; -use Swift_Mime_SimpleMimeEntity; +use Symfony\Component\Mime\Email; use Symfony\Component\Mime\Part\DataPart; class SentMessage { public $from = []; + public $to = []; + public $cc = []; + public $bcc = []; + public $replyTo = []; + public $subject = ''; + public $body = ''; + public $attachments = []; + public $headers = []; - public static function createFromSymfonyMailer(\Symfony\Component\Mime\Email $email): SentMessage + public static function createFromSymfonyMailer(Email $email): SentMessage { $sentMessage = new self(); @@ -54,25 +62,4 @@ public static function createFromSymfonyMailer(\Symfony\Component\Mime\Email $em return $sentMessage; } - - public static function createFromSwiftMailer(\Swift_Mime_SimpleMessage $message): SentMessage - { - $sentMessage = new self(); - - $sentMessage->from = $message->getFrom(); - $sentMessage->to = $message->getTo(); - $sentMessage->cc = $message->getCc(); - $sentMessage->bcc = $message->getBcc(); - $sentMessage->replyTo = $message->getReplyTo(); - $sentMessage->subject = $message->getSubject(); - $sentMessage->body = $message->getBody(); - $sentMessage->attachments = array_map(function(Swift_Mime_SimpleMimeEntity $entity) { - return [ - 'body' => $entity->getBody(), - 'disposition' => $entity->getContentType() . ' ' . $entity->getHeaders()->get('content-disposition'), - ]; - }, $message->getChildren()); - - return $sentMessage; - } } diff --git a/src/Store.php b/src/Store.php index 3a9f4f2..bd26544 100644 --- a/src/Store.php +++ b/src/Store.php @@ -6,6 +6,7 @@ use Carbon\Carbon; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\LazyCollection; class Store { @@ -14,11 +15,11 @@ class Store * * @return Collection|Email[] */ - public function getQueue(): Collection + public function getQueue(): LazyCollection { $query = new Email(); - return $query + return Email::query() ->whereNull('deleted_at') ->whereNull('sent_at') ->whereNull('queued_at') @@ -30,6 +31,6 @@ public function getQueue(): Collection ->where('attempts', '<', Config::maxAttemptCount()) ->orderBy('created_at', 'asc') ->limit(Config::cronjobEmailLimit()) - ->get(); + ->cursor(); } } diff --git a/src/Validator.php b/src/Validator.php deleted file mode 100644 index 69d130f..0000000 --- a/src/Validator.php +++ /dev/null @@ -1,224 +0,0 @@ -validateLabel($composer); - - $this->validateRecipient($composer); - - $this->validateCc($composer); - - $this->validateBcc($composer); - - $this->validateReplyTo($composer); - - $this->validateSubject($composer); - - $this->validateView($composer); - - $this->validateVariables($composer); - - $this->validateScheduled($composer); - } - - /** - * Validate the defined label. - * - * @param EmailComposer $composer - * @throws InvalidArgumentException - */ - private function validateLabel(EmailComposer $composer): void - { - if ($composer->hasData('label') && strlen($composer->getData('label')) > 255) { - throw new InvalidArgumentException('The given label [' . $composer->getData('label') . '] is too large for database storage'); - } - } - - /** - * Validate the given recipient(s). - * - * @param EmailComposer $composer - * @throws InvalidArgumentException - */ - private function validateRecipient(EmailComposer $composer): void - { - if (! $composer->hasData('recipient')) { - throw new InvalidArgumentException('No recipient specified'); - } - - $recipients = (array) $composer->getData('recipient'); - - if (count($recipients) == 0) { - throw new InvalidArgumentException('No recipient specified'); - } - - foreach ($recipients as $recipient) { - if (! filter_var($recipient, FILTER_VALIDATE_EMAIL)) { - throw new InvalidArgumentException('E-mail address [' . $recipient . '] is invalid'); - } - } - } - - /** - * Validate the carbon copy e-mail addresses. - * - * @param EmailComposer $composer - * @throws InvalidArgumentException - */ - private function validateCc(EmailComposer $composer): void - { - if (! $composer->hasData('cc')) { - return; - } - - foreach ((array) $composer->getData('cc') as $cc) { - if (! filter_var($cc, FILTER_VALIDATE_EMAIL)) { - throw new InvalidArgumentException('E-mail address [' . $cc . '] is invalid'); - } - } - } - - /** - * Validate the blind carbon copy e-mail addresses. - * - * @param EmailComposer $composer - * @throws InvalidArgumentException - */ - private function validateBcc(EmailComposer $composer): void - { - if (! $composer->hasData('bcc')) { - return; - } - - foreach ((array) $composer->getData('bcc') as $bcc) { - if (! filter_var($bcc, FILTER_VALIDATE_EMAIL)) { - throw new InvalidargumentException('E-mail address [' . $bcc . '] is invalid'); - } - } - } - - /** - * Validate the reply-to addresses. - * - * @param EmailComposer $composer - * @throws InvalidArgumentException - */ - private function validateReplyTo(EmailComposer $composer): void - { - if (! $composer->hasData('reply_to')) { - return; - } - - foreach (Arr::wrap($composer->getData('reply_to')) as $replyTo) { - if ($replyTo instanceof Address) { - $replyTo = $replyTo->address; - } - - if (! filter_var($replyTo, FILTER_VALIDATE_EMAIL)) { - throw new InvalidargumentException('E-mail address [' . $replyTo . '] is invalid'); - } - } - } - - /** - * Validate the e-mail subject. - * - * @param EmailComposer $composer - * @throws InvalidArgumentException - */ - private function validateSubject(EmailComposer $composer): void - { - if (! $composer->hasData('subject')) { - throw new InvalidArgumentException('No subject specified'); - } - } - - /** - * Validate the e-mail view. - * - * @param EmailComposer $composer - * @throws InvalidARgumentException - */ - private function validateView(EmailComposer $composer): void - { - if ($composer->hasData('mailable')) { - return; - } - - if (! $composer->hasData('view')) { - throw new InvalidArgumentException('No view specified'); - } - - $view = $composer->getData('view'); - - if (! view()->exists($view)) { - throw new InvalidArgumentException('View [' . $view . '] does not exist'); - } - } - - /** - * Validate the e-mail variables. - * - * @param EmailComposer $composer - * @throws InvalidArgumentException - */ - private function validateVariables(EmailComposer $composer): void - { - if ($composer->hasData('variables') && ! is_array($composer->getData('variables'))) { - throw new InvalidArgumentException('Variables must be an array'); - } - } - - /** - * Validate the scheduled date. - * - * @param EmailComposer $composer - * @throws InvalidArgumentException - */ - private function validateScheduled(EmailComposer $composer): void - { - if (! $composer->hasData('scheduled_at')) { - return; - } - - $scheduled = $composer->getData('scheduled_at'); - - if (! $scheduled instanceof Carbon && ! is_string($scheduled)) { - throw new InvalidArgumentException('Scheduled date must be a Carbon\Carbon instance or a strtotime-valid string'); - } - - if (is_string($scheduled)) { - try { - Carbon::parse($scheduled); - } catch (Exception $e) { - throw new InvalidArgumentException('Scheduled date could not be parsed by Carbon: ' . $e->getMessage()); - } - } - } -} diff --git a/testbench.yaml b/testbench.yaml new file mode 100644 index 0000000..2195c53 --- /dev/null +++ b/testbench.yaml @@ -0,0 +1,23 @@ +providers: + - Workbench\App\Providers\WorkbenchServiceProvider + - Stackkit\LaravelDatabaseEmails\LaravelDatabaseEmailsServiceProvider + +migrations: + - workbench/database/migrations + +seeders: + - Workbench\Database\Seeders\DatabaseSeeder + +workbench: + start: '/' + install: true + health: false + discovers: + web: true + api: false + commands: false + components: false + views: true + build: [] + assets: [] + sync: [] diff --git a/tests/ComposeTest.php b/tests/ComposeTest.php new file mode 100644 index 0000000..49e88b9 --- /dev/null +++ b/tests/ComposeTest.php @@ -0,0 +1,53 @@ + 'John Doe', + 'email' => 'johndoe@example.com', + 'password' => 'secret', + ]); + + $email = Email::compose() + ->user($user) + ->model($user) + ->envelope(fn (Envelope $envelope) => $envelope->subject('Hey')) + ->content(fn (Content $content) => $content->view('welcome')) + ->send(); + + $this->assertEquals($email->model_type, $user->getMorphClass()); + $this->assertEquals($email->model_id, $user->getKey()); + } + + #[Test] + public function models_can_be_empty(): void + { + $user = User::forceCreate([ + 'name' => 'John Doe', + 'email' => 'johndoe@example.com', + 'password' => 'secret', + ]); + + $email = Email::compose() + ->user($user) + ->envelope(fn (Envelope $envelope) => $envelope->subject('Hey')) + ->content(fn (Content $content) => $content->view('welcome')) + ->send(); + + $this->assertNull($email->model_type); + $this->assertNull($email->model_id); + } +} diff --git a/tests/ConfigCacheTest.php b/tests/ConfigCacheTest.php index 2851e6b..5c04075 100644 --- a/tests/ConfigCacheTest.php +++ b/tests/ConfigCacheTest.php @@ -2,18 +2,19 @@ namespace Tests; +use PHPUnit\Framework\Attributes\Test; use Throwable; class ConfigCacheTest extends TestCase { - /** @test */ + #[Test] public function the_configuration_file_can_be_cached() { $failed = false; try { - serialize(require __DIR__ . '/../config/laravel-database-emails.php'); - } catch (Throwable $e) { + serialize(require __DIR__.'/../config/database-emails.php'); + } catch (Throwable) { $failed = true; } diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index e6c376d..27bbba0 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -2,56 +2,47 @@ namespace Tests; +use PHPUnit\Framework\Attributes\Test; use Stackkit\LaravelDatabaseEmails\Config; class ConfigTest extends TestCase { - /** @test */ + #[Test] public function test_max_attempt_count() { $this->assertEquals(3, Config::maxAttemptCount()); - $this->app['config']->set('laravel-database-emails.attempts', 5); + $this->app['config']->set('database-emails.attempts', 5); $this->assertEquals(5, Config::maxAttemptCount()); } - /** @test */ - public function test_encrypt_emails() - { - $this->assertFalse(Config::encryptEmails()); - - $this->app['config']->set('laravel-database-emails.encrypt', true); - - $this->assertTrue(Config::encryptEmails()); - } - - /** @test */ + #[Test] public function test_testing() { $this->assertFalse(Config::testing()); - $this->app['config']->set('laravel-database-emails.testing.enabled', true); + $this->app['config']->set('database-emails.testing.enabled', true); $this->assertTrue(Config::testing()); } - /** @test */ + #[Test] public function test_test_email_address() { $this->assertEquals('test@email.com', Config::testEmailAddress()); - $this->app['config']->set('laravel-database-emails.testing.email', 'test+update@email.com'); + $this->app['config']->set('database-emails.testing.email', 'test+update@email.com'); $this->assertEquals('test+update@email.com', Config::testEmailAddress()); } - /** @test */ + #[Test] public function test_cronjob_email_limit() { $this->assertEquals(20, Config::cronjobEmailLimit()); - $this->app['config']->set('laravel-database-emails.limit', 15); + $this->app['config']->set('database-emails.limit', 15); $this->assertEquals(15, Config::cronjobEmailLimit()); } diff --git a/tests/DatabaseInteractionTest.php b/tests/DatabaseInteractionTest.php index 1bc8d71..11fdb6d 100644 --- a/tests/DatabaseInteractionTest.php +++ b/tests/DatabaseInteractionTest.php @@ -3,32 +3,35 @@ namespace Tests; use Carbon\Carbon; +use Illuminate\Mail\Mailables\Address; use Illuminate\Support\Facades\DB; +use PHPUnit\Framework\Attributes\Test; +use Stackkit\LaravelDatabaseEmails\Attachment; class DatabaseInteractionTest extends TestCase { - /** @test */ + #[Test] public function label_should_be_saved_correctly() { $email = $this->sendEmail(['label' => 'welcome-email']); $this->assertEquals('welcome-email', DB::table('emails')->find(1)->label); - $this->assertEquals('welcome-email', $email->getLabel()); + $this->assertEquals('welcome-email', $email->label); } - /** @test */ + #[Test] public function recipient_should_be_saved_correctly() { $email = $this->sendEmail(['recipient' => 'john@doe.com']); - $this->assertEquals('john@doe.com', $email->getRecipient()); + $this->assertEquals(['john@doe.com' => null], $email->recipient); } - /** @test */ + #[Test] public function cc_and_bcc_should_be_saved_correctly() { $email = $this->sendEmail([ - 'cc' => $cc = [ + 'cc' => $cc = [ 'john@doe.com', ], 'bcc' => $bcc = [ @@ -36,76 +39,54 @@ public function cc_and_bcc_should_be_saved_correctly() ], ]); - $this->assertEquals(json_encode($cc), DB::table('emails')->find(1)->cc); - $this->assertTrue($email->hasCc()); - $this->assertEquals(['john@doe.com'], $email->getCc()); - $this->assertEquals(json_encode($bcc), DB::table('emails')->find(1)->bcc); - $this->assertTrue($email->hasBcc()); - $this->assertEquals(['jane@doe.com'], $email->getBcc()); + $this->assertEquals(['john@doe.com' => null], $email->cc); + $this->assertEquals(['jane@doe.com' => null], $email->bcc); } - /** @test */ + #[Test] public function reply_to_should_be_saved_correctly() { $email = $this->sendEmail([ - 'reply_to' => $replyTo = [ + 'reply_to' => [ 'john@doe.com', ], ]); - $this->assertEquals(json_encode($replyTo), DB::table('emails')->find(1)->reply_to); - $this->assertTrue($email->hasReplyTo()); - $this->assertEquals(['john@doe.com'], $email->getReplyTo()); + $this->assertEquals(['john@doe.com' => null], $email->reply_to); } - /** @test */ + #[Test] public function subject_should_be_saved_correclty() { $email = $this->sendEmail(['subject' => 'test subject']); $this->assertEquals('test subject', DB::table('emails')->find(1)->subject); - $this->assertEquals('test subject', $email->getSubject()); + $this->assertEquals('test subject', $email->subject); } - /** @test */ + #[Test] public function view_should_be_saved_correctly() { $email = $this->sendEmail(['view' => 'tests::dummy']); $this->assertEquals('tests::dummy', DB::table('emails')->find(1)->view); - $this->assertEquals('tests::dummy', $email->getView()); + $this->assertEquals('tests::dummy', $email->view); } - /** @test */ - public function encrypted_should_be_saved_correctly() - { - $email = $this->sendEmail(); - - $this->assertEquals(0, DB::table('emails')->find(1)->encrypted); - $this->assertFalse($email->isEncrypted()); - - $this->app['config']['laravel-database-emails.encrypt'] = true; - - $email = $this->sendEmail(); - - $this->assertEquals(1, DB::table('emails')->find(2)->encrypted); - $this->assertTrue($email->isEncrypted()); - } - - /** @test */ + #[Test] public function scheduled_date_should_be_saved_correctly() { $email = $this->sendEmail(); $this->assertNull(DB::table('emails')->find(1)->scheduled_at); - $this->assertNull($email->getScheduledDate()); + $this->assertNull($email->scheduled_at); Carbon::setTestNow(Carbon::create(2019, 1, 1, 1, 2, 3)); $email = $this->scheduleEmail('+2 weeks'); $this->assertNotNull(DB::table('emails')->find(2)->scheduled_at); - $this->assertEquals('2019-01-15 01:02:03', $email->getScheduledDate()); + $this->assertEquals('2019-01-15 01:02:03', $email->scheduled_at); } - /** @test */ + #[Test] public function the_body_should_be_saved_correctly() { $email = $this->sendEmail(['variables' => ['name' => 'Jane Doe']]); @@ -113,44 +94,44 @@ public function the_body_should_be_saved_correctly() $expectedBody = "Name: Jane Doe\n"; $this->assertSame($expectedBody, DB::table('emails')->find(1)->body); - $this->assertSame($expectedBody, $email->getBody()); + $this->assertSame($expectedBody, $email->body); } - /** @test */ + #[Test] public function from_should_be_saved_correctly() { $email = $this->composeEmail()->send(); - $this->assertFalse($email->hasFrom()); - $this->assertEquals(config('mail.from.address'), $email->getFromAddress()); - $this->assertEquals(config('mail.from.name'), $email->getFromName()); + $this->assertEquals($email->from['address'], $email->from['address']); + $this->assertEquals($email->from['name'], $email->from['name']); - $email = $this->composeEmail()->from('marick@dolphiq.nl', 'Marick')->send(); + $email = $this->composeEmail([ + 'from' => new Address('marick@dolphiq.nl', 'Marick'), + ])->send(); - $this->assertTrue($email->hasFrom()); - $this->assertEquals('marick@dolphiq.nl', $email->getFromAddress()); - $this->assertEquals('Marick', $email->getFromName()); + $this->assertTrue((bool) $email->from); + $this->assertEquals('marick@dolphiq.nl', $email->from['address']); + $this->assertEquals('Marick', $email->from['name']); } - /** @test */ + #[Test] public function variables_should_be_saved_correctly() { $email = $this->sendEmail(['variables' => ['name' => 'John Doe']]); - $this->assertEquals(json_encode(['name' => 'John Doe'], 1), DB::table('emails')->find(1)->variables); - $this->assertEquals(['name' => 'John Doe'], $email->getVariables()); + $this->assertEquals(['name' => 'John Doe'], $email->variables); } - /** @test */ + #[Test] public function the_sent_date_should_be_null() { $email = $this->sendEmail(); $this->assertNull(DB::table('emails')->find(1)->sent_at); - $this->assertNull($email->getSendDate()); + $this->assertNull($email->sent_at); } - /** @test */ + #[Test] public function failed_should_be_zero() { $email = $this->sendEmail(); @@ -159,16 +140,16 @@ public function failed_should_be_zero() $this->assertFalse($email->hasFailed()); } - /** @test */ + #[Test] public function attempts_should_be_zero() { $email = $this->sendEmail(); $this->assertEquals(0, DB::table('emails')->find(1)->attempts); - $this->assertEquals(0, $email->getAttempts()); + $this->assertEquals(0, $email->attempts); } - /** @test */ + #[Test] public function the_scheduled_date_should_be_saved_correctly() { Carbon::setTestNow(Carbon::now()); @@ -177,62 +158,55 @@ public function the_scheduled_date_should_be_saved_correctly() $email = $this->scheduleEmail('+2 weeks'); - $this->assertTrue($email->isScheduled()); - $this->assertEquals($scheduledFor, $email->getScheduledDate()); + $this->assertEquals($scheduledFor, $email->scheduled_at); } - /** @test */ + #[Test] public function recipient_should_be_swapped_for_test_address_when_in_testing_mode() { - $this->app['config']->set('laravel-database-emails.testing.enabled', function () { + $this->app['config']->set('database-emails.testing.enabled', function () { return true; }); - $this->app['config']->set('laravel-database-emails.testing.email', 'test@address.com'); + $this->app['config']->set('database-emails.testing.email', 'test@address.com'); $email = $this->sendEmail(['recipient' => 'jane@doe.com']); - $this->assertEquals('test@address.com', $email->getRecipient()); + $this->assertEquals(['test@address.com' => null], $email->recipient); } - /** @test */ + #[Test] public function attachments_should_be_saved_correctly() { $email = $this->composeEmail() - ->attach(__DIR__ . '/files/pdf-sample.pdf') - ->send(); - - $this->assertCount(1, $email->getAttachments()); - - $attachment = $email->getAttachments()[0]; - - $this->assertEquals('attachment', $attachment['type']); - $this->assertEquals(__DIR__ . '/files/pdf-sample.pdf', $attachment['attachment']['file']); - - $email = $this->composeEmail() - ->attach(__DIR__ . '/files/pdf-sample.pdf') - ->attach(__DIR__ . '/files/pdf-sample-2.pdf') + ->attachments([ + Attachment::fromPath(__DIR__.'/files/pdf-sample.pdf'), + Attachment::fromPath(__DIR__.'/files/pdf-sample2.pdf'), + Attachment::fromStorageDisk('my-custom-disk', 'pdf-sample-2.pdf'), + ]) ->send(); - $this->assertCount(2, $email->getAttachments()); + $this->assertCount(3, $email->attachments); - $this->assertEquals(__DIR__ . '/files/pdf-sample.pdf', $email->getAttachments()[0]['attachment']['file']); - $this->assertEquals(__DIR__ . '/files/pdf-sample-2.pdf', $email->getAttachments()[1]['attachment']['file']); + $this->assertEquals( + [ + 'path' => __DIR__.'/files/pdf-sample.pdf', + 'disk' => null, + 'as' => null, + 'mime' => null, + ], + $email->attachments[0] + ); } - /** @test */ - public function in_memory_attachments_should_be_saved_correctly() + #[Test] + public function in_memory_attachments_are_not_supported() { - $rawData = file_get_contents(__DIR__ . '/files/pdf-sample.pdf'); + $this->expectExceptionMessage('Raw attachments are not supported in the database email driver.'); - $email = $this->composeEmail() - ->attachData($rawData, 'generated.pdf', [ - 'mime' => 'application/pdf', + $this->composeEmail() + ->attachments([ + Attachment::fromData(fn () => file_get_contents(__DIR__.'/files/pdf-sample.pdf'), 'pdf-sample'), ]) ->send(); - - $this->assertCount(1, $email->getAttachments()); - - $this->assertEquals('rawAttachment', $email->getAttachments()[0]['type']); - $this->assertEquals(md5($rawData), md5($email->getAttachments()[0]['attachment']['data'])); } } diff --git a/tests/EncryptionTest.php b/tests/EncryptionTest.php deleted file mode 100644 index 36a9328..0000000 --- a/tests/EncryptionTest.php +++ /dev/null @@ -1,133 +0,0 @@ -app['config']['laravel-database-emails.encrypt'] = true; - - $this->sendEmail(); - } - - /** @test */ - public function an_email_should_be_marked_as_encrypted() - { - $email = $this->sendEmail(); - - $this->assertTrue($email->isEncrypted()); - } - - /** @test */ - public function the_recipient_should_be_encrypted_and_decrypted() - { - $email = $this->sendEmail(['recipient' => 'john@doe.com']); - - $this->assertEquals('john@doe.com', decrypt($email->getRawDatabaseValue('recipient'))); - - $this->assertEquals('john@doe.com', $email->getRecipient()); - } - - /** @test */ - public function cc_and_bb_should_be_encrypted_and_decrypted() - { - $email = $this->sendEmail([ - 'cc' => $cc = ['john+1@doe.com', 'john+2@doe.com'], - 'bcc' => $bcc = ['jane+1@doe.com', 'jane+2@doe.com'], - ]); - - $this->assertEquals($cc, decrypt($email->getRawDatabaseValue('cc'))); - $this->assertEquals($bcc, decrypt($email->getRawDatabaseValue('bcc'))); - - $this->assertEquals($cc, $email->getCc()); - $this->assertEquals($bcc, $email->getBcc()); - } - - /** @test */ - public function reply_to_should_be_encrypted_and_decrypted() - { - $email = $this->sendEmail([ - 'reply_to' => $replyTo = ['john+1@doe.com', 'john+2@doe.com'], - ]); - $this->assertEquals($replyTo, decrypt($email->getRawDatabaseValue('reply_to'))); - $this->assertEquals($replyTo, $email->getReplyTo()); - - if (! class_exists(Address::class)) { - return; - } - - // Test with a single Address object... - $email = $this->sendEmail([ - 'reply_to' => new Address('john+1@doe.com', 'John Doe'), - ]); - $this->assertEquals([['address' => 'john+1@doe.com', 'name' => 'John Doe']], decrypt($email->getRawDatabaseValue('reply_to'))); - $this->assertEquals([['address' => 'john+1@doe.com', 'name' => 'John Doe']], $email->getReplyTo()); - - // Address with an array of Address objects... - $email = $this->sendEmail([ - 'reply_to' => [ - new Address('john+1@doe.com', 'John Doe'), - new Address('jane+1@doe.com', 'Jane Doe'), - ], - ]); - $this->assertSame([['address' => 'john+1@doe.com', 'name' => 'John Doe'], ['address' => 'jane+1@doe.com', 'name' => 'Jane Doe']], decrypt($email->getRawDatabaseValue('reply_to'))); - $this->assertSame([['address' => 'john+1@doe.com', 'name' => 'John Doe'], ['address' => 'jane+1@doe.com', 'name' => 'Jane Doe']], $email->getReplyTo()); - } - - /** @test */ - public function the_subject_should_be_encrypted_and_decrypted() - { - $email = $this->sendEmail(['subject' => 'test subject']); - - $this->assertEquals('test subject', decrypt($email->getRawDatabaseValue('subject'))); - - $this->assertEquals('test subject', $email->getSubject()); - } - - /** @test */ - public function the_variables_should_be_encrypted_and_decrypted() - { - $email = $this->sendEmail(['variables' => ['name' => 'Jane Doe']]); - - $this->assertEquals( - ['name' => 'Jane Doe'], - decrypt($email->getRawDatabaseValue('variables')) - ); - - $this->assertEquals( - ['name' => 'Jane Doe'], - $email->getVariables() - ); - } - - /** @test */ - public function the_body_should_be_encrypted_and_decrypted() - { - $email = $this->sendEmail(['variables' => ['name' => 'Jane Doe']]); - - $expectedBody = "Name: Jane Doe\n"; - - $this->assertEquals($expectedBody, decrypt($email->getRawDatabaseValue('body'))); - - $this->assertEquals($expectedBody, $email->getBody()); - } - - /** @test */ - public function from_should_be_encrypted_and_decrypted() - { - $email = $this->composeEmail()->from('marick@dolphiq.nl', 'Marick')->send(); - - $expect = [ - 'address' => 'marick@dolphiq.nl', - 'name' => 'Marick', - ]; - - $this->assertEquals($expect, decrypt($email->getRawDatabaseValue('from'))); - $this->assertEquals($expect, $email->getFrom()); - } -} diff --git a/tests/EnvelopeTest.php b/tests/EnvelopeTest.php new file mode 100644 index 0000000..4e29bc1 --- /dev/null +++ b/tests/EnvelopeTest.php @@ -0,0 +1,134 @@ +envelope( + (new Envelope()) + ->subject('Hey') + ->from('asdf@gmail.com') + ->to(['johndoe@example.com', 'janedoe@example.com']) + ) + ->content( + (new Content()) + ->view('tests::dummy') + ->with(['name' => 'John Doe']) + ) + ->send(); + + $this->assertEquals([ + 'johndoe@example.com' => null, + 'janedoe@example.com' => null, + ], $email->recipient); + } + + #[Test] + public function test_it_can_pass_user_models() + { + $user = (new User())->forceFill([ + 'email' => 'johndoe@example.com', + 'name' => 'J. Doe', + ]); + + $email = Email::compose() + ->user($user) + ->envelope(fn (Envelope $envelope) => $envelope->subject('Hey')) + ->content(fn (Content $content) => $content->view('welcome')) + ->send(); + + $this->assertEquals( + [ + 'johndoe@example.com' => 'J. Doe', + ], + $email->recipient + ); + } + + #[Test] + public function users_can_have_a_preferred_email() + { + $user = (new UserWithPreferredEmail())->forceFill([ + 'email' => 'johndoe@example.com', + 'name' => 'J. Doe', + ]); + + $email = Email::compose() + ->user($user) + ->envelope(fn (Envelope $envelope) => $envelope->subject('Hey')) + ->content(fn (Content $content) => $content->view('welcome')) + ->send(); + + $this->assertEquals( + [ + 'noreply@abc.com' => 'J. Doe', + ], + $email->recipient + ); + } + + #[Test] + public function users_can_have_a_preferred_name() + { + $user = (new UserWithPreferredName())->forceFill([ + 'email' => 'johndoe@example.com', + 'name' => 'J. Doe', + ]); + + $email = Email::compose() + ->user($user) + ->envelope(fn (Envelope $envelope) => $envelope->subject('Hey')) + ->content(fn (Content $content) => $content->view('welcome')) + ->send(); + + $this->assertEquals( + [ + 'johndoe@example.com' => 'J.D.', + ], + $email->recipient + ); + } + + #[Test] + public function users_can_have_a_preferred_locale() + { + $nonLocaleUser = (new User())->forceFill([ + 'email' => 'johndoe@example.com', + 'name' => 'J. Doe', + ]); + + $emailForNonLocaleUser = Email::compose() + ->user($nonLocaleUser) + ->envelope(fn (Envelope $envelope) => $envelope->subject('Hey')) + ->content(fn (Content $content) => $content->view('locale-email')) + ->send(); + + $localeUser = (new UserWithPreferredLocale())->forceFill([ + 'email' => 'johndoe@example.com', + 'name' => 'J. Doe', + ]); + + $emailForLocaleUser = Email::compose() + ->user($localeUser) + ->envelope(fn (Envelope $envelope) => $envelope->subject('Hey')) + ->content(fn (Content $content) => $content->view('locale-email')) + ->send(); + + $this->assertStringContainsString('Hello!', $emailForNonLocaleUser->body); + $this->assertStringContainsString('Kumusta!', $emailForLocaleUser->body); + } +} diff --git a/tests/MailableReaderTest.php b/tests/MailableReaderTest.php index c8fbd11..7e15865 100644 --- a/tests/MailableReaderTest.php +++ b/tests/MailableReaderTest.php @@ -4,98 +4,90 @@ use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Address; -use Illuminate\Mail\Mailables\Attachment; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; +use PHPUnit\Framework\Attributes\Test; +use Stackkit\LaravelDatabaseEmails\Attachment; use Stackkit\LaravelDatabaseEmails\Email; class MailableReaderTest extends TestCase { private function mailable(): Mailable { - if (version_compare(app()->version(), '10.0.0', '>=')) { - return new Laravel10TestMailable(); - } - return new TestMailable(); } - /** @test */ + #[Test] public function it_extracts_the_recipient() { $composer = Email::compose() ->mailable($this->mailable()); - $this->assertEquals(['john@doe.com'], $composer->getData('recipient')); + $this->assertEquals(['john@doe.com' => 'John Doe'], $composer->email->recipient); $composer = Email::compose() ->mailable( $this->mailable()->to(['jane@doe.com']) ); - $this->assertCount(2, $composer->getData('recipient')); - $this->assertContains('john@doe.com', $composer->getData('recipient')); - $this->assertContains('jane@doe.com', $composer->getData('recipient')); + $this->assertCount(2, $composer->email->recipient); + $this->assertArrayHasKey('john@doe.com', $composer->email->recipient); + $this->assertArrayHasKey('jane@doe.com', $composer->email->recipient); } - /** @test */ + #[Test] public function it_extracts_cc_addresses() { $composer = Email::compose()->mailable($this->mailable()); - $this->assertEquals(['john+cc@doe.com', 'john+cc2@doe.com'], $composer->getData('cc')); + $this->assertEquals(['john+cc@doe.com' => null, 'john+cc2@doe.com' => null], $composer->email->cc); } - /** @test */ + #[Test] public function it_extracts_bcc_addresses() { $composer = Email::compose()->mailable($this->mailable()); - $this->assertEquals(['john+bcc@doe.com', 'john+bcc2@doe.com'], $composer->getData('bcc')); + $this->assertEquals(['john+bcc@doe.com' => null, 'john+bcc2@doe.com' => null], $composer->email->bcc); } - /** @test */ + #[Test] public function it_extracts_reply_to_addresses() { $composer = Email::compose()->mailable($this->mailable()); - $this->assertEquals(['replyto@example.com', 'replyto2@example.com'], $composer->getData('reply_to')); + $this->assertEquals(['replyto@example.com' => null, 'replyto2@example.com' => null], $composer->email->reply_to); } - /** @test */ + #[Test] public function it_extracts_the_subject() { $composer = Email::compose()->mailable($this->mailable()); - $this->assertEquals('Your order has shipped!', $composer->getData('subject')); + $this->assertEquals('Your order has shipped!', $composer->email->subject); } - /** @test */ + #[Test] public function it_extracts_the_body() { $composer = Email::compose()->mailable($this->mailable()); - $this->assertEquals("Name: John Doe\n", $composer->getData('body')); + $this->assertEquals("Name: John Doe\n", $composer->email->body); } - /** @test */ + #[Test] public function it_extracts_attachments() { $email = Email::compose()->mailable($this->mailable())->send(); - $attachments = $email->getAttachments(); + $attachments = $email->attachments; $this->assertCount(2, $attachments); - $this->assertEquals('attachment', $attachments[0]['type']); - $this->assertEquals(__DIR__ . '/files/pdf-sample.pdf', $attachments[0]['attachment']['file']); - - $this->assertEquals('rawAttachment', $attachments[1]['type']); - $this->assertEquals('order.html', $attachments[1]['attachment']['name']); - $this->assertEquals('

Thanks for your oder

', $attachments[1]['attachment']['data']); + $this->assertEquals(__DIR__.'/files/pdf-sample.pdf', $attachments[0]['path']); } - /** @test */ + #[Test] public function it_extracts_the_from_address_and_or_name() { $email = Email::compose()->mailable( @@ -103,59 +95,30 @@ public function it_extracts_the_from_address_and_or_name() ->from('marick@dolphiq.nl', 'Marick') )->send(); - $this->assertTrue($email->hasFrom()); - $this->assertEquals('marick@dolphiq.nl', $email->getFromAddress()); - $this->assertEquals('Marick', $email->getFromName()); + $this->assertTrue((bool) $email->from); + $this->assertEquals('marick@dolphiq.nl', $email->from['address']); + $this->assertEquals('Marick', $email->from['name']); $email = Email::compose()->mailable( ($this->mailable()) ->from('marick@dolphiq.nl') )->send(); - $this->assertTrue($email->hasFrom()); - $this->assertEquals('marick@dolphiq.nl', $email->getFromAddress()); - $this->assertEquals(config('mail.from.name'), $email->getFromName()); + $this->assertTrue((bool) $email->from); + $this->assertEquals('marick@dolphiq.nl', $email->from['address']); + $this->assertEquals(null, $email->from['name']); $email = Email::compose()->mailable( ($this->mailable()) - ->from(null, 'Marick') + ->from('marick@dolphiq.nl', 'Marick') )->send(); - // 8.x no longer accepts an empty address. - // https://github.com/laravel/framework/pull/39035 - if (version_compare(app()->version(), '8.0.0', '>=')) { - $this->assertFalse($email->hasFrom()); - } else { - $this->assertTrue($email->hasFrom()); - $this->assertEquals(config('mail.from.address'), $email->getFromAddress()); - $this->assertEquals('Marick', $email->getFromName()); - } + $this->assertEquals('marick@dolphiq.nl', $email->from['address']); + $this->assertEquals('Marick', $email->from['name']); } } class TestMailable extends Mailable -{ - /** - * Build the message. - * - * @return $this - */ - public function build() - { - return $this->to('john@doe.com') - ->cc(['john+cc@doe.com', 'john+cc2@doe.com']) - ->bcc(['john+bcc@doe.com', 'john+bcc2@doe.com']) - ->replyTo(['replyto@example.com', 'replyto2@example.com']) - ->subject('Your order has shipped!') - ->attach(__DIR__ . '/files/pdf-sample.pdf', [ - 'mime' => 'application/pdf', - ]) - ->attachData('

Thanks for your oder

', 'order.html') - ->view('tests::dummy', ['name' => 'John Doe']); - } -} - -class Laravel10TestMailable extends Mailable { public function content(): Content { @@ -173,7 +136,7 @@ public function envelope(): Envelope return new Envelope( null, [ - new Address('john@doe.com', 'John Doe') + new Address('john@doe.com', 'John Doe'), ], ['john+cc@doe.com', 'john+cc2@doe.com'], ['john+bcc@doe.com', 'john+bcc2@doe.com'], @@ -185,10 +148,8 @@ public function envelope(): Envelope public function attachments(): array { return [ - Attachment::fromPath(__DIR__ . '/files/pdf-sample.pdf')->withMime('application/pdf'), - Attachment::fromData(function () { - return '

Thanks for your oder

'; - }, 'order.html') + Attachment::fromPath(__DIR__.'/files/pdf-sample.pdf')->withMime('application/pdf'), + Attachment::fromStorageDisk(__DIR__.'/files/pdf-sample.pdf', 'my-local-disk')->withMime('application/pdf'), ]; } } diff --git a/tests/PruneTest.php b/tests/PruneTest.php index 03a51cc..b562f35 100644 --- a/tests/PruneTest.php +++ b/tests/PruneTest.php @@ -2,22 +2,22 @@ namespace Tests; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Carbon; +use PHPUnit\Framework\Attributes\Test; use Stackkit\LaravelDatabaseEmails\Email; class PruneTest extends TestCase { - /** @test */ + #[Test] public function by_default_mails_are_pruned_after_6_months() { $email = $this->sendEmail(); - Carbon::setTestNow($email->created_at . ' + 6 months'); + Carbon::setTestNow($email->created_at.' + 6 months'); $this->artisan('model:prune', ['--model' => [Email::class]]); $this->assertInstanceOf(Email::class, $email->fresh()); - Carbon::setTestNow($email->created_at . ' + 6 months + 1 day'); + Carbon::setTestNow($email->created_at.' + 6 months + 1 day'); // Ensure the email object has to be passed manually, otherwise we are acidentally // deleting everyone's e-mails... @@ -29,7 +29,7 @@ public function by_default_mails_are_pruned_after_6_months() $this->assertNull($email->fresh()); } - /** @test */ + #[Test] public function can_change_when_emails_are_pruned() { Email::pruneWhen(function (Email $email) { @@ -38,11 +38,11 @@ public function can_change_when_emails_are_pruned() $email = $this->sendEmail(); - Carbon::setTestNow($email->created_at . ' + 3 months'); + Carbon::setTestNow($email->created_at.' + 3 months'); $this->artisan('model:prune', ['--model' => [Email::class]]); $this->assertInstanceOf(Email::class, $email->fresh()); - Carbon::setTestNow($email->created_at . ' + 3 months + 1 day'); + Carbon::setTestNow($email->created_at.' + 3 months + 1 day'); $this->artisan('model:prune', ['--model' => [Email::class]]); $this->assertNull($email->fresh()); } diff --git a/tests/QueuedEmailsTest.php b/tests/QueuedEmailsTest.php index 0bb7b96..124f57f 100644 --- a/tests/QueuedEmailsTest.php +++ b/tests/QueuedEmailsTest.php @@ -2,34 +2,35 @@ namespace Tests; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Queue; +use PHPUnit\Framework\Attributes\Test; use Stackkit\LaravelDatabaseEmails\SendEmailJob; -use Illuminate\Support\Facades\Mail; +use Workbench\App\Jobs\CustomSendEmailJob; class QueuedEmailsTest extends TestCase { - public function setUp(): void - { - parent::setUp(); - } - - /** @test */ + #[Test] public function queueing_an_email_will_leave_sending_on_false() { + Queue::fake(); + $email = $this->queueEmail(); $this->assertEquals(0, $email->sending); } - /** @test */ + #[Test] public function queueing_an_email_will_set_the_queued_at_column() { + Queue::fake(); + $email = $this->queueEmail(); $this->assertNotNull($email->queued_at); } - /** @test */ + #[Test] public function queueing_an_email_will_dispatch_a_job() { Queue::fake(); @@ -41,7 +42,7 @@ public function queueing_an_email_will_dispatch_a_job() }); } - /** @test */ + #[Test] public function emails_can_be_queued_on_a_specific_connection() { Queue::fake(); @@ -53,7 +54,7 @@ public function emails_can_be_queued_on_a_specific_connection() }); } - /** @test */ + #[Test] public function emails_can_be_queued_on_a_specific_queue() { Queue::fake(); @@ -65,7 +66,7 @@ public function emails_can_be_queued_on_a_specific_queue() }); } - /** @test */ + #[Test] public function emails_can_be_queued_with_a_delay() { Queue::fake(); @@ -79,7 +80,7 @@ public function emails_can_be_queued_with_a_delay() }); } - /** @test */ + #[Test] public function the_send_email_job_will_call_send_on_the_email_instance() { Queue::fake(); @@ -93,7 +94,7 @@ public function the_send_email_job_will_call_send_on_the_email_instance() $job->handle(); } - /** @test */ + #[Test] public function the_mail_will_be_marked_as_sent_when_job_is_finished() { Queue::fake(); @@ -105,4 +106,14 @@ public function the_mail_will_be_marked_as_sent_when_job_is_finished() $this->assertTrue($email->isSent()); } + + #[Test] + public function developers_can_choose_their_own_job() + { + Queue::fake(); + + $email = $this->queueEmail(jobClass: CustomSendEmailJob::class); + + Queue::assertPushed(fn (CustomSendEmailJob $job) => $job->email->id === $email->id); + } } diff --git a/tests/SendEmailsCommandTest.php b/tests/SendEmailsCommandTest.php index ccd38d7..ae447ed 100644 --- a/tests/SendEmailsCommandTest.php +++ b/tests/SendEmailsCommandTest.php @@ -5,48 +5,49 @@ use Carbon\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Queue; +use PHPUnit\Framework\Attributes\Test; use Stackkit\LaravelDatabaseEmails\Store; class SendEmailsCommandTest extends TestCase { - /** @test */ + #[Test] public function an_email_should_be_marked_as_sent() { $email = $this->sendEmail(); $this->artisan('email:send'); - $this->assertNotNull($email->fresh()->getSendDate()); + $this->assertNotNull($email->fresh()->sent_at); } - /** @test */ + #[Test] public function the_number_of_attempts_should_be_incremented() { $email = $this->sendEmail(); - $this->assertEquals(0, $email->fresh()->getAttempts()); + $this->assertEquals(0, $email->fresh()->attempts); $this->artisan('email:send'); - $this->assertEquals(1, $email->fresh()->getAttempts()); + $this->assertEquals(1, $email->fresh()->attempts); } - /** @test */ + #[Test] public function an_email_should_not_be_sent_once_it_is_marked_as_sent() { $email = $this->sendEmail(); $this->artisan('email:send'); - $this->assertNotNull($firstSend = $email->fresh()->getSendDate()); + $this->assertNotNull($firstSend = $email->fresh()->sent_at); $this->artisan('email:send'); - $this->assertEquals(1, $email->fresh()->getAttempts()); - $this->assertEquals($firstSend, $email->fresh()->getSendDate()); + $this->assertEquals(1, $email->fresh()->attempts); + $this->assertEquals($firstSend, $email->fresh()->sent_at); } - /** @test */ + #[Test] public function an_email_should_not_be_sent_if_it_is_queued() { Queue::fake(); @@ -55,53 +56,53 @@ public function an_email_should_not_be_sent_if_it_is_queued() $this->artisan('email:send'); - $this->assertNull($email->fresh()->getSendDate()); + $this->assertNull($email->fresh()->sent_at); } - /** @test */ + #[Test] public function if_an_email_fails_to_be_sent_it_should_be_logged_in_the_database() { $email = $this->sendEmail(); - $email->update(['recipient' => 'asdf']); + $email->update(['recipient' => ['asdf' => null]]); $this->artisan('email:send'); $this->assertTrue($email->fresh()->hasFailed()); - $this->assertStringContainsString('RfcComplianceException', $email->fresh()->getError()); + $this->assertStringContainsString('RfcComplianceException', $email->fresh()->error); } - /** @test */ + #[Test] public function the_number_of_emails_sent_per_minute_should_be_limited() { for ($i = 1; $i <= 30; $i++) { $this->sendEmail(); } - $this->app['config']['laravel-database-emails.limit'] = 25; + $this->app['config']['database-emails.limit'] = 25; $this->artisan('email:send'); $this->assertEquals(5, DB::table('emails')->whereNull('sent_at')->count()); } - /** @test */ + #[Test] public function an_email_should_never_be_sent_before_its_scheduled_date() { $email = $this->scheduleEmail(Carbon::now()->addHour(1)); $this->artisan('email:send'); $email = $email->fresh(); - $this->assertEquals(0, $email->getAttempts()); - $this->assertNull($email->getSendDate()); + $this->assertEquals(0, $email->attempts); + $this->assertNull($email->sent_at); $email->update(['scheduled_at' => Carbon::now()->toDateTimeString()]); $this->artisan('email:send'); $email = $email->fresh(); - $this->assertEquals(1, $email->getAttempts()); - $this->assertNotNull($email->getSendDate()); + $this->assertEquals(1, $email->attempts); + $this->assertNotNull($email->sent_at); } - /** @test */ + #[Test] public function emails_will_be_sent_until_max_try_count_has_been_reached() { $this->app['config']['mail.driver'] = 'does-not-exist'; @@ -116,23 +117,23 @@ public function emails_will_be_sent_until_max_try_count_has_been_reached() $this->assertCount(0, (new Store)->getQueue()); } - /** @test */ + #[Test] public function the_failed_status_and_error_is_cleared_if_a_previously_failed_email_is_sent_succesfully() { $email = $this->sendEmail(); $email->update([ - 'failed' => true, - 'error' => 'Simulating some random error', + 'failed' => true, + 'error' => 'Simulating some random error', 'attempts' => 1, ]); - $this->assertTrue($email->fresh()->hasFailed()); - $this->assertEquals('Simulating some random error', $email->fresh()->getError()); + $this->assertTrue($email->fresh()->failed); + $this->assertEquals('Simulating some random error', $email->fresh()->error); $this->artisan('email:send'); - $this->assertFalse($email->fresh()->hasFailed()); - $this->assertEmpty($email->fresh()->getError()); + $this->assertFalse($email->fresh()->failed); + $this->assertEmpty($email->fresh()->error); } } diff --git a/tests/SenderTest.php b/tests/SenderTest.php index eb86b66..7083b01 100644 --- a/tests/SenderTest.php +++ b/tests/SenderTest.php @@ -4,33 +4,31 @@ use Illuminate\Mail\Mailables\Address; use Illuminate\Support\Facades\Event; -use Stackkit\LaravelDatabaseEmails\MessageSent; -use Stackkit\LaravelDatabaseEmails\SentMessage; -use Swift_Events_SendEvent; use Illuminate\Support\Facades\Mail; +use PHPUnit\Framework\Attributes\Test; +use RuntimeException; +use Stackkit\LaravelDatabaseEmails\Attachment; use Stackkit\LaravelDatabaseEmails\Email; +use Stackkit\LaravelDatabaseEmails\MessageSent; +use Stackkit\LaravelDatabaseEmails\SentMessage; class SenderTest extends TestCase { - /** @var Swift_Events_SendEvent[] */ + /** @var array */ public $sent = []; public function setUp(): void { parent::setUp(); - if (version_compare(app()->version(), '9.0.0', '>=')) { - Event::listen(MessageSent::class, function (MessageSent $event) { - $this->sent[] = SentMessage::createFromSymfonyMailer( - $event->message->getSymfonySentMessage()->getOriginalMessage() - ); - }); - } else { - Mail::getSwiftMailer()->registerPlugin(new TestingMailEventListener($this)); - } + Event::listen(MessageSent::class, function (MessageSent $event) { + $this->sent[] = SentMessage::createFromSymfonyMailer( + $event->message->getSymfonySentMessage()->getOriginalMessage() + ); + }); } - /** @test */ + #[Test] public function it_sends_an_email() { $this->sendEmail(); @@ -40,7 +38,7 @@ public function it_sends_an_email() $this->artisan('email:send'); } - /** @test */ + #[Test] public function the_email_has_a_correct_from_email_and_from_name() { $this->app['config']->set('mail.from.address', 'testfromaddress@gmail.com'); @@ -58,7 +56,7 @@ public function the_email_has_a_correct_from_email_and_from_name() // custom from... $this->sent = []; - $this->composeEmail()->from('marick@dolphiq.nl', 'Marick')->send(); + $this->composeEmail(['from' => new Address('marick@dolphiq.nl', 'Marick')])->send(); $this->artisan('email:send'); $from = reset($this->sent)->from; $this->assertEquals('marick@dolphiq.nl', key($from)); @@ -66,22 +64,14 @@ public function the_email_has_a_correct_from_email_and_from_name() // only address $this->sent = []; - $this->composeEmail()->from('marick@dolphiq.nl')->send(); + $this->composeEmail(['from' => 'marick@dolphiq.nl'])->send(); $this->artisan('email:send'); $from = reset($this->sent)->from; $this->assertEquals('marick@dolphiq.nl', key($from)); - $this->assertEquals(config('mail.from.name'), $from[key($from)]); - - // only name - $this->sent = []; - $this->composeEmail()->from(null, 'Marick')->send(); - $this->artisan('email:send'); - $from = reset($this->sent)->from; - $this->assertEquals(config('mail.from.address'), key($from)); - $this->assertEquals('Marick', $from[key($from)]); + $this->assertEquals(null, $from[key($from)]); } - /** @test */ + #[Test] public function it_sends_emails_to_the_correct_recipients() { $this->sendEmail(['recipient' => 'john@doe.com']); @@ -99,7 +89,7 @@ public function it_sends_emails_to_the_correct_recipients() $this->assertArrayHasKey('john+2@doe.com', $to); } - /** @test */ + #[Test] public function it_adds_the_cc_addresses() { $this->sendEmail(['cc' => 'cc@test.com']); @@ -117,7 +107,7 @@ public function it_adds_the_cc_addresses() $this->assertArrayHasKey('cc+2@test.com', $cc); } - /** @test */ + #[Test] public function it_adds_the_bcc_addresses() { $this->sendEmail(['bcc' => 'bcc@test.com']); @@ -135,7 +125,7 @@ public function it_adds_the_bcc_addresses() $this->assertArrayHasKey('bcc+2@test.com', $bcc); } - /** @test */ + #[Test] public function the_email_has_the_correct_subject() { $this->sendEmail(['subject' => 'Hello World']); @@ -147,7 +137,7 @@ public function the_email_has_the_correct_subject() $this->assertEquals('Hello World', $subject); } - /** @test */ + #[Test] public function the_email_has_the_correct_body() { $this->sendEmail(['variables' => ['name' => 'John Doe']]); @@ -162,64 +152,48 @@ public function the_email_has_the_correct_body() $this->assertEquals(view('tests::dummy'), $body); } - /** @test */ + #[Test] public function attachments_are_added_to_the_email() { $this->composeEmail() - ->attach(__DIR__ . '/files/pdf-sample.pdf') + ->attachments([ + Attachment::fromPath(__DIR__.'/files/pdf-sample.pdf'), + Attachment::fromPath(__DIR__.'/files/my-file.txt')->as('Test123 file'), + Attachment::fromStorageDisk('my-custom-disk', 'test.txt'), + ]) ->send(); $this->artisan('email:send'); $attachments = reset($this->sent)->attachments; - $this->assertCount(1, $attachments); + $this->assertCount(3, $attachments); + $this->assertEquals('Test123'."\n", $attachments[1]['body']); + $this->assertEquals('text/plain disposition: attachment filename: Test123 file', $attachments[1]['disposition']); + $this->assertEquals("my file from public disk\n", $attachments[2]['body']); } - /** @test */ - public function raw_attachments_are_added_to_the_email() + #[Test] + public function raw_attachments_are_not_added_to_the_email() { - $rawData = file_get_contents(__DIR__ . '/files/pdf-sample.pdf'); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Raw attachments are not supported in the database email driver.'); $this->composeEmail() - ->attachData($rawData, 'hello-ci.pdf', [ - 'mime' => 'application/pdf', + ->attachments([ + Attachment::fromData(fn () => 'test', 'test.txt'), ]) ->send(); - $this->artisan('email:send'); - - $attachments = reset($this->sent)->attachments; - $attachment = reset($attachments); - - $this->assertCount(1, $attachments); - $this->assertStringContainsString('hello-ci.pdf', $attachment['disposition']); - $this->assertStringContainsString('application/pdf', $attachment['disposition']); - $this->assertTrue(md5($attachment['body']) == md5($rawData)); - } - - /** @test */ - public function old_json_encoded_attachments_can_still_be_read() - { - $email = $this->sendEmail(); - $email->attachments = json_encode([1, 2, 3]); - $email->save(); - - $this->assertEquals([1, 2, 3], $email->fresh()->getAttachments()); - - $email->attachments = serialize([4, 5, 6]); - $email->save(); - - $this->assertEquals([4, 5, 6], $email->fresh()->getAttachments()); } - /** @test */ + #[Test] public function emails_can_be_sent_immediately() { - $this->app['config']->set('laravel-database-emails.immediately', false); + $this->app['config']->set('database-emails.immediately', false); $this->sendEmail(); $this->assertCount(0, $this->sent); Email::truncate(); - $this->app['config']->set('laravel-database-emails.immediately', true); + $this->app['config']->set('database-emails.immediately', true); $this->sendEmail(); $this->assertCount(1, $this->sent); @@ -227,7 +201,7 @@ public function emails_can_be_sent_immediately() $this->assertCount(1, $this->sent); } - /** @test */ + #[Test] public function it_adds_the_reply_to_addresses() { $this->sendEmail(['reply_to' => 'replyto@test.com']); @@ -244,10 +218,6 @@ public function it_adds_the_reply_to_addresses() $this->assertArrayHasKey('replyto1@test.com', $replyTo); $this->assertArrayHasKey('replyto2@test.com', $replyTo); - if (! class_exists(Address::class)) { - return; - } - $this->sent = []; $this->sendEmail([ 'reply_to' => new Address('replyto@test.com', 'NoReplyTest'), diff --git a/tests/TestCase.php b/tests/TestCase.php index 482aded..08dbe88 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,12 +2,20 @@ namespace Tests; +use Illuminate\Foundation\Testing\LazilyRefreshDatabase; +use Illuminate\Mail\Mailables\Content; +use Illuminate\Mail\Mailables\Envelope; +use Orchestra\Testbench\Concerns\WithWorkbench; use Stackkit\LaravelDatabaseEmails\Email; +use Stackkit\LaravelDatabaseEmails\LaravelDatabaseEmailsServiceProvider; class TestCase extends \Orchestra\Testbench\TestCase { protected $invalid; + use LazilyRefreshDatabase; + use WithWorkbench; + public function setUp(): void { parent::setUp(); @@ -24,52 +32,57 @@ function () { }, ]; - view()->addNamespace('tests', __DIR__ . '/views'); + view()->addNamespace('tests', __DIR__.'/views'); - $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); + $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); Email::truncate(); } - /** - * Get package providers. At a minimum this is the package being tested, but also - * would include packages upon which our package depends, e.g. Cartalyst/Sentry - * In a normal app environment these would be added to the 'providers' array in - * the config/app.php file. - * - * @param \Illuminate\Foundation\Application $app - * - * @return array - */ protected function getPackageProviders($app) { return [ - \Stackkit\LaravelDatabaseEmails\LaravelDatabaseEmailsServiceProvider::class, + LaravelDatabaseEmailsServiceProvider::class, ]; } /** * Define environment setup. * - * @param \Illuminate\Foundation\Application $app + * @param \Illuminate\Foundation\Application $app * @return void */ protected function getEnvironmentSetUp($app) { - $app['config']->set('laravel-database-emails.attempts', 3); - $app['config']->set('laravel-database-emails.testing.enabled', false); - $app['config']->set('laravel-database-emails.testing.email', 'test@email.com'); + $app['config']->set('database-emails.attempts', 3); + $app['config']->set('database-emails.testing.enabled', false); + $app['config']->set('database-emails.testing.email', 'test@email.com'); + + $app['config']->set('filesystems.disks.my-custom-disk', [ + 'driver' => 'local', + 'root' => __DIR__.'/../workbench/storage/app/public', + ]); $app['config']->set('database.default', 'testbench'); + $driver = env('DB_DRIVER', 'sqlite'); $app['config']->set('database.connections.testbench', [ - 'driver' => getenv('CI_DB_DRIVER'), - 'host' => getenv('CI_DB_HOST'), - 'port' => getenv('CI_DB_PORT'), - 'database' => getenv('CI_DB_DATABASE'), - 'username' => getenv('CI_DB_USERNAME'), - 'password' => getenv('CI_DB_PASSWORD'), - 'prefix' => '', - 'strict' => true, + 'driver' => $driver, + ...match ($driver) { + 'sqlite' => [ + 'database' => ':memory:', + ], + 'mysql' => [ + 'host' => '127.0.0.1', + 'port' => 3307, + ], + 'pgsql' => [ + 'host' => '127.0.0.1', + 'port' => 5432, + ], + }, + 'database' => 'test', + 'username' => 'test', + 'password' => 'test', ]); $app['config']->set('mail.driver', 'log'); @@ -78,25 +91,30 @@ protected function getEnvironmentSetUp($app) public function createEmail($overwrite = []) { $params = array_merge([ - 'label' => 'welcome', + 'label' => 'welcome', 'recipient' => 'john@doe.com', - 'cc' => null, - 'bcc' => null, - 'reply_to' => null, - 'subject' => 'test', - 'view' => 'tests::dummy', + 'cc' => null, + 'bcc' => null, + 'reply_to' => null, + 'subject' => 'test', + 'view' => 'tests::dummy', 'variables' => ['name' => 'John Doe'], + 'from' => null, ], $overwrite); return Email::compose() ->label($params['label']) - ->recipient($params['recipient']) - ->cc($params['cc']) - ->bcc($params['bcc']) - ->replyTo($params['reply_to']) - ->subject($params['subject']) - ->view($params['view']) - ->variables($params['variables']); + ->envelope(fn (Envelope $envelope) => $envelope + ->to($params['recipient']) + ->when($params['cc'], fn ($envelope) => $envelope->cc($params['cc'])) + ->when($params['bcc'], fn ($envelope) => $envelope->bcc($params['bcc'])) + ->when($params['reply_to'], fn ($envelope) => $envelope->replyTo($params['reply_to'])) + ->when($params['from'], fn (Envelope $envelope) => $envelope->from($params['from'])) + ->subject($params['subject'])) + ->content(fn (Content $content) => $content + ->view($params['view']) + ->with($params['variables']) + ); } public function composeEmail($overwrite = []) @@ -111,11 +129,11 @@ public function sendEmail($overwrite = []) public function scheduleEmail($scheduledFor, $overwrite = []) { - return $this->createEmail($overwrite)->schedule($scheduledFor); + return $this->createEmail($overwrite)->later($scheduledFor); } - public function queueEmail($connection = null, $queue = null, $delay = null, $overwrite = []) + public function queueEmail($connection = null, $queue = null, $delay = null, $overwrite = [], ?string $jobClass = null) { - return $this->createEmail($overwrite)->queue($connection, $queue, $delay); + return $this->createEmail($overwrite)->queue($connection, $queue, $delay, $jobClass); } } diff --git a/tests/TestingMailEventListener.php b/tests/TestingMailEventListener.php deleted file mode 100644 index 4fa32f1..0000000 --- a/tests/TestingMailEventListener.php +++ /dev/null @@ -1,22 +0,0 @@ -test = $test; - } - - public function beforeSendPerformed(\Swift_Events_Event $event) - { - $this->test->sent[] = SentMessage::createFromSwiftMailer($event->getMessage()); - } -} diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php deleted file mode 100644 index b8b02ce..0000000 --- a/tests/ValidatorTest.php +++ /dev/null @@ -1,205 +0,0 @@ -expectException(InvalidArgumentException::class); - - Email::compose() - ->label(str_repeat('a', 256)) - ->send(); - } - - /** @test */ - public function a_recipient_is_required() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No recipient specified'); - - Email::compose() - ->send(); - } - - /** @test */ - public function a_recipient_cannot_be_empty() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No recipient specified'); - - Email::compose() - ->recipient([]) - ->send(); - } - - /** @test */ - public function the_recipient_email_must_be_valid() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('E-mail address [not-a-valid-email-address] is invalid'); - - Email::compose() - ->recipient('not-a-valid-email-address') - ->send(); - } - - /** @test */ - public function cc_must_contain_valid_email_addresses() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('E-mail address [not-a-valid-email-address] is invalid'); - - Email::compose() - ->recipient('john@doe.com') - ->cc([ - 'jane@doe.com', - 'not-a-valid-email-address', - ]) - ->send(); - } - - /** @test */ - public function bcc_must_contain_valid_email_addresses() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('E-mail address [not-a-valid-email-address] is invalid'); - - Email::compose() - ->recipient('john@doe.com') - ->bcc([ - 'jane@doe.com', - 'not-a-valid-email-address', - ]) - ->send(); - } - - /** @test */ - public function reply_to_must_contain_valid_email_addresses() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('E-mail address [not-a-valid-email-address] is invalid'); - - Email::compose() - ->recipient('john@doe.com') - ->replyTo([ - 'jane@doe.com', - 'not-a-valid-email-address', - ]) - ->send(); - } - - /** @test */ - public function a_subject_is_required() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No subject specified'); - - Email::compose() - ->recipient('john@doe.com') - ->send(); - } - - /** @test */ - public function a_view_is_required() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No view specified'); - - Email::compose() - ->recipient('john@doe.com') - ->subject('test') - ->send(); - } - - /** @test */ - public function the_view_must_exist() - { - // this view exists, if error thrown -> fail test - try { - Email::compose() - ->recipient('john@doe.com') - ->subject('test') - ->view('tests::dummy') - ->send(); - } catch (InvalidArgumentException $e) { - $this->fail('Expected view [tests::dummy] to exist but it does not'); - } - - // this view does not exist -> expect exception - $this->expectException(InvalidArgumentException::class); - - Email::compose() - ->recipient('john@doe.com') - ->subject('test') - ->view('tests::does-not-exist') - ->send(); - } - - /** @test */ - public function variables_must_be_defined_as_an_array() - { - $email = Email::compose() - ->recipient('john@doe.com') - ->subject('test') - ->view('tests::dummy'); - - foreach ($this->invalid as $type) { - try { - $email->variables($type)->send(); - $this->fail('Expected exception to be thrown'); - } catch (\TypeError $e) { - $this->assertEquals($e->getCode(), 0); - } - } - - $valid = []; - - try { - $email->variables($valid)->send(); - } catch (InvalidArgumentException $e) { - $this->fail('Did not expect exception to be thrown'); - } - } - - /** @test */ - public function the_scheduled_date_must_be_a_carbon_instance_or_a_valid_date() - { - // invalid - foreach ($this->invalid as $value) { - try { - Email::compose() - ->recipient('john@doe.com') - ->subject('test') - ->view('tests::dummy') - ->schedule($value); - $this->fail('Expected exception to be thrown'); - } catch (InvalidArgumentException $e) { - $this->assertEquals(0, $e->getCode()); - } - } - - // valid - try { - Email::compose() - ->recipient('john@doe.com') - ->subject('test') - ->view('tests::dummy') - ->schedule('+2 week'); - - Email::compose() - ->recipient('john@doe.com') - ->subject('test') - ->view('tests::dummy') - ->schedule(Carbon\Carbon::now()); - } catch (InvalidArgumentException $e) { - $this->fail('Dit not expect exception to be thrown'); - } - } -} diff --git a/tests/files/my-file.txt b/tests/files/my-file.txt new file mode 100644 index 0000000..8f214dd --- /dev/null +++ b/tests/files/my-file.txt @@ -0,0 +1 @@ +Test123 diff --git a/workbench/app/Jobs/CustomSendEmailJob.php b/workbench/app/Jobs/CustomSendEmailJob.php new file mode 100644 index 0000000..ddd3ddd --- /dev/null +++ b/workbench/app/Jobs/CustomSendEmailJob.php @@ -0,0 +1,11 @@ +loadTranslationsFrom( + __DIR__.'/../../lang', + 'package' + ); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/workbench/bootstrap/.gitkeep b/workbench/bootstrap/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/workbench/bootstrap/app.php b/workbench/bootstrap/app.php new file mode 100644 index 0000000..6ead72a --- /dev/null +++ b/workbench/bootstrap/app.php @@ -0,0 +1,19 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + ) + ->withMiddleware(function (Middleware $middleware) { + // + }) + ->withExceptions(function (Exceptions $exceptions) { + // + })->create(); diff --git a/workbench/bootstrap/providers.php b/workbench/bootstrap/providers.php new file mode 100644 index 0000000..3ac44ad --- /dev/null +++ b/workbench/bootstrap/providers.php @@ -0,0 +1,5 @@ + 'Hello!', +]; diff --git a/workbench/lang/fil-PH/messages.php b/workbench/lang/fil-PH/messages.php new file mode 100644 index 0000000..f801763 --- /dev/null +++ b/workbench/lang/fil-PH/messages.php @@ -0,0 +1,5 @@ + 'Kumusta!', +]; diff --git a/workbench/resources/views/.gitkeep b/workbench/resources/views/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/workbench/resources/views/locale-email.blade.php b/workbench/resources/views/locale-email.blade.php new file mode 100644 index 0000000..6d1b8a4 --- /dev/null +++ b/workbench/resources/views/locale-email.blade.php @@ -0,0 +1,2 @@ +{{ trans('workbench::messages.greeting') }} + diff --git a/workbench/routes/.gitkeep b/workbench/routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/workbench/routes/console.php b/workbench/routes/console.php new file mode 100644 index 0000000..eff2ed2 --- /dev/null +++ b/workbench/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote')->hourly(); diff --git a/workbench/routes/web.php b/workbench/routes/web.php new file mode 100644 index 0000000..86a06c5 --- /dev/null +++ b/workbench/routes/web.php @@ -0,0 +1,7 @@ +