diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md new file mode 100644 index 0000000..ba1c3d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue-template.md @@ -0,0 +1,14 @@ +--- +name: Issue template +about: Help you create an effective issue and know what to expect. +title: '' +labels: '' +assignees: '' + +--- + +Thank you for taking the time for reporting a bug, requesting a feature, or letting me know something else about the package. + +I create open-source packages for fun while working full-time and running my own business. That means I don't have as much time left to maintain these packages, build elaborate new features or investigate and fix bugs. If you wish to get a feature or bugfix merged it would be greatly appreciated if you can provide as much info as possible and preferably a Pull Request ready with automated tests. Realistically I check Github a few times a week, and take several days, weeks or sometimes months before finishing features/bugfixes (depending on their size of course). + +Thanks for understanding. 😁 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..0fb5351 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,109 @@ +name: Run tests + +on: + pull_request_target: + types: [opened, synchronize, labeled] + schedule: + - cron: '0 0 * * *' + +jobs: + access_check: + runs-on: ubuntu-latest + name: Access check + steps: + - name: Ensure pull-request is safe to run + uses: actions/github-script@v7 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + if (context.eventName === 'schedule') { + return + } + + // If the user that pushed the commit is a maintainer, skip the check + const collaborators = await github.rest.repos.listCollaborators({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + if (collaborators.data.some(c => c.login === context.actor)) { + console.log(`User ${context.actor} is allowed to run tests because they are a collaborator.`); + return + } + + const issue_number = context.issue.number; + const repository = context.repo.repo; + const owner = context.repo.owner; + + const response = await github.rest.issues.listLabelsOnIssue({ + owner, + repo: repository, + issue_number + }); + const labels = response.data.map(label => label.name); + let hasLabel = labels.includes('safe-to-test') + + if (context.payload.action === 'synchronize' && hasLabel) { + hasLabel = false + await github.rest.issues.removeLabel({ + owner, + repo: repository, + issue_number, + name: 'safe-to-test' + }); + } + + if (!hasLabel) { + throw "Action was not authorized. Exiting now." + } + + php-tests: + runs-on: ubuntu-latest + needs: access_check + strategy: + matrix: + db: [ 'mysql', 'sqlite', 'pgsql' ] + payload: + - { laravel: '11.*', php: '8.3', 'testbench': '9.*', collision: '8.*' } + - { laravel: '11.*', php: '8.2', 'testbench': '9.*', collision: '8.*' } + - { laravel: '12.*', php: '8.2', 'testbench': '10.*', collision: '8.*' } + - { laravel: '12.*', php: '8.3', 'testbench': '10.*', collision: '8.*' } + - { laravel: '12.*', php: '8.4', 'testbench': '10.*', collision: '8.*' } + + name: PHP ${{ matrix.payload.php }} - Laravel ${{ matrix.payload.laravel }} - DB ${{ matrix.db }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.payload.php }} + extensions: mbstring, dom, fileinfo, mysql + coverage: none + + - name: Set up MySQL and PostgreSQL + run: | + if [ "${{ matrix.db }}" != "sqlite" ]; then + MYSQL_PORT=3307 POSTGRES_PORT=5432 docker compose up ${{ matrix.db }} -d + fi + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.payload.laravel }}" "orchestra/testbench:${{ matrix.payload.testbench }}" "nunomaduro/collision:${{ matrix.payload.collision }}" --no-interaction --no-update + composer update --prefer-stable --prefer-dist --no-interaction + if [ "${{ matrix.db }}" = "mysql" ]; then + while ! mysqladmin ping --host=127.0.0.1 --user=test --port=3307 --password=test --silent; do + echo "Waiting for MySQL..." + sleep 1 + done + else + echo "Not waiting for MySQL." + fi + - name: Execute tests + env: + DB_DRIVER: ${{ matrix.db }} + run: composer test diff --git a/.gitignore b/.gitignore index b3aec00..b730e63 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ vendor/ .DS_Store composer.lock +.phpunit.result.cache +.phpunit.cache +test + diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index 6894532..0000000 --- a/.styleci.yml +++ /dev/null @@ -1,7 +0,0 @@ -preset: laravel - -enabled: - - concat_with_spaces - -disabled: - - concat_without_spaces diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 545e6f9..0000000 --- a/.travis.yml +++ /dev/null @@ -1,35 +0,0 @@ -language: php - -sudo: false - -services: - - mysql - -matrix: - fast_finish: true - include: - # SQLite - - php: 7.0 - env: CI_DB_DRIVER="sqlite" CI_DB_DATABASE=":memory:" - - php: 7.1 - env: CI_DB_DRIVER="sqlite" CI_DB_DATABASE=":memory:" - # MySQL 5.7 - - php: 7.0 - env: CI_DB_DRIVER="mysql" CI_DB_HOST="127.0.0.1" CI_DB_DATABASE="travis" CI_DB_USERNAME="root" - - php: 7.1 - env: CI_DB_DRIVER="mysql" CI_DB_HOST="127.0.0.1" CI_DB_DATABASE="travis" CI_DB_USERNAME="root" - # MariaDB - - php: 7.0 - env: CI_DB_DRIVER="mysql" CI_DB_HOST="127.0.0.1" CI_DB_DATABASE="travis" CI_DB_USERNAME="root" - addons: - mariadb: 10.0 - - php: 7.1 - env: CI_DB_DRIVER="mysql" CI_DB_HOST="127.0.0.1" CI_DB_DATABASE="travis" CI_DB_USERNAME="root" - addons: - mariadb: 10.0 - -install: travis_retry composer install --no-interaction --prefer-source - -script: - - mysql --version - - vendor/bin/phpunit --verbose diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..201d443 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,227 @@ +# Releases +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## 7.0.0 - 2024-03-24 + +**Added** + +- Laravel 10 and 11 support +- Customizable job class for queueing +- Index on emails table to improve performance +- Added support for SQLite and PostgreSQL + +**Changed** + +- Email::compose() has changed. See UPGRADING.md +- Old email table is incompatible - new table will be created + +**Removed** + +- Support for Laravel 6, 7, 8 and 9 +- Email encyption + +## 6.3.0 - 2023-12-30 + +**Added** + +- ReplyTo feature + +## 6.2.1 - 2023-12-28 + +**Changed** + +- Test package with PHP 8.3 + +## 6.2.0 - 2023-04-09 + +**Added** + +- Added support for Laravel 10 style mailables + +## 6.1.0 - 2023-02-08 + +**Changed** + +- Added support for Laravel 10 + +## 6.0.0 - 2022-02-10 + +**Added** + +- Added support for Laravel 9 with new Symfony Mailer instead of SwiftMail. + +**Changed** + +- Dropped support for Laravel 5.6, 5.7 and 5.8. + +## 5.0.0 - 2021-12-05 + +**Added** + +- Option to switch from auto-loaded migrations to manually published. Useful for using a multi tenant Laravel app (stancl/tenancy for example). + +**Fixed** + +- Before, when an email was queued using `queue()` it could still be sent using the `email:send` command, thus resulting in duplicate email sends. This has been fixed by adding a `queued_at` column. + +## 4.2.0 - 2020-05-16 + +**Added** + +- Support for Laravel 7.x +- Queued option + +## 4.1.1 - 2020-01-11 + +**Fixed** + +- Fixed inline attachments could not be stored +- Fixed PHP 7.4 issue when reading empty Mailable from address + +## 4.1.0 - 2019-07-13 + +**Added** + +- Option to send e-mails immediately after calling send() or later() + +**Changed** + +- attach() and attachData() will no longer add empty or null files + +## 4.0.2 - 2019-01-01 + +**Fixed** + +- Fixed regression bug (testing mode) + +## 4.0.1 - 2018-12-31 + +**Added** + +- New environment variable `LARAVEL_DATABASE_EMAILS_TESTING_ENABLED` to indicate if testing mode is enabled (*) + +**Fixed** + +- Fixed issue where Mailables would not be read correctly +- Config file was not cachable (*) + +(*) = To be able to cache the config file, change the 'testing' closure to the environment variable as per `laravel-database-emails.php` config file. + +## 4.0.0 - 2018-09-15 + +**Changed** + +- Changed package namespace + +**Removed** + +- Removed resend/retry option entirely +- Removed process time limit + +## 3.0.3 - 2018-07-24 + +**Fixed** + +- Transforming an `Email` object to JSON would cause the encrpyted attributes to stay encrypted. This is now fixed. + +## 3.0.2 - 2018-03-22 + +**Changed** + +- Updated README.md + +**Added** + +- Support for process time limit + +--- + +## 3.0.1 - 2018-03-18 + +**Changed** + +- Updated README.md +- Deprecated `email:retry`, please use `email:resend` + +--- + +## 3.0.0 - 2017-12-22 + +**Added** + +- Support for a custom sender per e-mail. + +**Upgrade from 2.x to 3.x** + +3.0.0 added support for a custom sender per e-mail. To update please run the following command: + +```bash +php artisan migrate +``` + +--- + +## 2.0.0 - 2017-12-14 + +**Added** + +- Support for multiple recipients, cc and bcc addresses. +- Support for mailables (*) +- Support for attachments +- New method `later` + +*= Only works for Laravel versions 5.5 and up because 5.5 finally introduced a method to read the mailable body. + +**Fixed** +- Bug causing failed e-mails not to be resent + +**Upgrade from 1.x to 2.x** +Because 2.0.0 introduced support for attachments, the database needs to be updated. Simply run the following two commands after updating your dependencies and running composer update: + +```bash +php artisan migrate +``` + +--- + +## 1.1.3 - 2017-12-07 + +**Fixed** + +- Created a small backwards compatibility fix for Laravel versions 5.4 and below. + +--- + +## 1.1.2 - 2017-11-18 + +**Fixed** + +- Incorrect auto discovery namespace for Laravel 5.5 + +--- + +## 1.1.1 - 2017-08-02 + +**Changed** + +- Only dispatch `before.send` event during unit tests + +--- + +## 1.1.0 - 2017-07-01 + +**Added** + +- PHPUnit tests +- Support for CC and BCC + +--- + +## 1.0.0 - 2017-06-29 + +**Added** + +- Initial release of the package diff --git a/README.md b/README.md index d8b8b01..a9fd99f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,256 @@ -

- -

-

-Build Status -Latest Stable Version -License -

+[![Run tests](https://github.com/stackkit/laravel-database-emails/actions/workflows/run-tests.yml/badge.svg)](https://github.com/stackkit/laravel-database-emails/actions/workflows/run-tests.yml) +[![Latest Version on Packagist](https://poser.pugx.org/stackkit/laravel-database-emails/v/stable.svg)](https://packagist.org/packages/stackkit/laravel-database-emails) +[![Total Downloads](https://poser.pugx.org/stackkit/laravel-database-emails/downloads.svg)](https://packagist.org/packages/stackkit/laravel-database-emails) -## Introduction +# Introduction -This package allows you to easily send e-mails using a database table. +This package allows you to store and send e-mails using the database. -Official documentation, changelog and more [is located here](https://stackkit.github.io/laravel-database-emails/). +# Requirements + +This package requires Laravel 11 or 12. + +# Installation + +Require the package using composer. + +```shell +composer require stackkit/laravel-database-emails +``` + +Publish the configuration files. + +```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. + +```shell +php artisan migrate +``` + +Add the e-mail cronjob to your scheduler + +```php +protected function schedule(Schedule $schedule) +{ + $schedule->command('email:send')->everyMinute()->withoutOverlapping(5); +} +``` + + +# Usage + +### Send an email + +E-mails are composed the same way mailables are created. + +```php +use Stackkit\LaravelDatabaseEmails\Email; +use Illuminate\Mail\Mailables\Content; +use Stackkit\LaravelDatabaseEmails\Attachment; +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(); +]) +``` + +### Sending emails to users in your application + +```php +Email::compose() + ->user($user) + ->send(); +``` + +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 +class User extends Model +{ + public function preferredEmailName(): string + { + return $this->first_name; + } +} +``` + +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 +class User extends Model +{ + public function preferredEmailAddress(): string + { + return $this->work_email; + } +} +``` + +By default, the app locale will be used. If you wish to use something different, you should implement the `preferredEmailLocale` method in your model. + +```php +class User extends Model implements HasLocalePreference +{ + public function preferredLocale(): string + { + return $this->locale; + } +} +``` + +### Using mailables + +You may also pass a mailable to the e-mail composer. + +```php +Email::compose() + ->mailable(new OrderShipped()) + ->send(); +``` + +### Attachments + +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. + + +```php +use Stackkit\LaravelDatabaseEmails\Attachment; + +Email::compose() + ->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(); +``` + +> [!NOTE] +> `Attachment::fromData()` and `Attachment::fromStorage()` are not supported as they work with raw data. + +### Attaching models to e-mails + +You may attach a model to an e-mail. This can be useful to attach a user or another model that belongs to the e-mail. + +```php +Email::compose() + ->model(User::find(1)); +``` + +### Scheduling + +You may schedule an e-mail by calling `later` instead of `send`. You must provide a Carbon instance or a strtotime valid date. + +```php +Email::compose() + ->later('+2 hours'); +``` + +### Queueing e-mails + +> [!IMPORTANT] +> When queueing mail using the `queue` function, it is no longer necessary to schedule the `email:send` command. + +```php +Email::compose()->queue(); + +// On a specific connection +Email::compose()->queue(connection: 'sqs'); + +// On a specific queue +Email::compose()->queue(queue: 'email-queue'); + +// 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: + +```php +Email::compose()->queue(jobClass: CustomSendEmailJob::class); +``` + +It could look like this: + +```php +command('model:prune', [ + '--model' => [Email::class], +])->daily(); +``` + +By default, e-mails are pruned when they are older than 6 months. + +You may change that by adding the following to the AppServiceProvider.php: + +```php +use Stackkit\LaravelDatabaseEmails\Email; + +public function register(): void +{ + Email::pruneWhen(function (Email $email) { + return $email->where(...); + }); +} +``` 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 7baa82b..720c56d 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "buildcode/laravel-database-emails", + "name": "stackkit/laravel-database-emails", "description": "Store and send e-mails using the database", "license": "MIT", "authors": [ @@ -10,29 +10,63 @@ ], "autoload": { "psr-4": { - "Buildcode\\LaravelDatabaseEmails\\": "src/" + "Stackkit\\LaravelDatabaseEmails\\": "src/" } }, "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": { "laravel": { "providers": [ - "Buildcode\\LaravelDatabaseEmails\\LaravelDatabaseEmailsServiceProvider" + "Stackkit\\LaravelDatabaseEmails\\LaravelDatabaseEmailsServiceProvider" ] } }, + "require": { + "ext-json": "*", + "laravel/framework": "^11.0|^12.0", + "doctrine/dbal": "^4.0" + }, "require-dev": { - "illuminate/database": "5.5.*", - "illuminate/console": "5.5.*", - "illuminate/validation": "5.5.*", - "orchestra/testbench": "^3.5", - "orchestra/database": "^3.5", - "phpunit/phpunit": "^6.0", - "mockery/mockery": "^1.0", - "dompdf/dompdf": "^0.8.2" + "mockery/mockery": "^1.2", + "orchestra/testbench": "^9.0|^10.0", + "nunomaduro/collision": "^8.0", + "laravel/pint": "^1.14" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "scripts": { + "l11": [ + "composer update laravel/framework:11.* orchestra/testbench:9.* nunomaduro/collision:8.* --with-all-dependencies" + ], + "l12": [ + "composer update laravel/framework:12.* orchestra/testbench:10.* nunomaduro/collision:8.* --with-all-dependencies" + ], + "test": [ + "testbench workbench:create-sqlite-db", + "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 73% rename from config/laravel-database-emails.php rename to config/database-emails.php index c35a229..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,13 +28,9 @@ 'testing' => [ - 'email' => 'test@email.com', + 'email' => env('DB_EMAILS_TESTING_EMAIL'), - 'enabled' => function () { - return false; - // ...or... - // return app()->environment('local', 'staging'); - }, + 'enabled' => env('DB_EMAILS_TESTING_ENABLED', false), ], @@ -55,11 +39,24 @@ | Cronjob Limit |-------------------------------------------------------------------------- | - | Limit the number of e-mails that should be sent at a time. Please ajust this + | Limit the number of e-mails that should be sent at a time. Please adjust this | configuration based on the number of e-mails you expect to send and | the throughput of your e-mail sending provider. | */ - 'limit' => 20, + 'limit' => env('DB_EMAILS_LIMIT', 20), + + /* + |-------------------------------------------------------------------------- + | Send E-mails Immediately + |-------------------------------------------------------------------------- + | + | Sends e-mails immediately after calling send() or schedule(). Useful for development + | when you don't have Laravel Scheduler running or don't want to wait up to + | 60 seconds for each e-mail to be sent. + | + */ + + '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/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..f8d1d06 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +services: + app: + image: serversideup/php:8.3-fpm + volumes: + - .:/var/www/html + 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 13c3008..a1ec749 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,33 +1,22 @@ - - - - - ./tests/ - - - - - - - - - - - - - - - src/ - - + + + + ./tests/ + + + + + + + + + + + + + + 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 a31e727..689eff4 100644 --- a/src/Config.php +++ b/src/Config.php @@ -1,62 +1,48 @@ '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 = []; - /** - * Compose a new e-mail. - * - * @return EmailComposer - */ - public static function compose() - { - return new EmailComposer(new self); - } - - /** - * Get the e-mail id. - * - * @return int - */ - public function getId() - { - return $this->id; - } - - /** - * Get the e-mail label. - * - * @return string|null - */ - public function getLabel() - { - return $this->label; - } - - /** - * Get the e-mail recipient. - * - * @return string - */ - public function getRecipient() - { - return $this->recipient; - } - - /** - * Get the e-mail recipient. - * - * @return string - */ - public function getRecipientAttribute() - { - return $this->recipient; - } - - /** - * Get the e-mail from. - * - * @return string - */ - public function getFrom() - { - return $this->from; - } - - /** - * Get the e-mail from. - * - * @return string - */ - public function getFromAttribute() - { - return $this->from; - } - - /** - * Get the e-mail from address. - * - * @return string|null - */ - public function getFromAddress() - { - return $this->from['address'] ?? config('mail.from.address'); - } - - /** - * Get the e-mail from address. - * - * @return string|null - */ - public function getFromName() - { - return $this->from['name'] ?? config('mail.from.name'); - } - - /** - * Get the e-mail recipient(s) as string. - * - * @return string - */ - public function getRecipientsAsString() - { - $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 - */ - 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 - */ - public function getBccAttribute() - { - return $this->bcc; - } - - /** - * Get the e-mail subject. - * - * @return string - */ - public function getSubject() - { - return $this->subject; - } - - /** - * Get the e-mail subject. - * - * @return string - */ - public function getSubjectAttribute() - { - return $this->subject; - } - - /** - * Get the e-mail view. - * - * @return string - */ - public function getView() - { - return $this->view; - } - - /** - * Get the e-mail variables. - * - * @return array - */ - public function getVariables() - { - return $this->variables; - } - - /** - * Get the e-mail variables. - * - * @return array - */ - public function getVariablesAttribute() - { - return $this->variables; - } - - /** - * Get the e-mail body. - * - * @return string - */ - public function getBody() - { - return $this->body; - } - - /** - * Get the e-mail body. - * - * @return string - */ - public function getBodyAttribute() - { - return $this->body; - } - - /** - * Get the e-mail attachments. - * - * @return array - */ - public function getAttachments() - { - return $this->attachments; - } + public static ?Closure $pruneQuery = null; - /** - * Get the number of times this e-mail was attempted to send. - * - * @return int - */ - public function getAttempts() + public static function compose(): EmailComposer { - return $this->attempts; + return new EmailComposer(new static); } - /** - * Get the scheduled date. - * - * @return mixed - */ - public function getScheduledDate() - { - return $this->scheduled_at; - } - - /** - * Determine if the e-mail has variables defined. - * - * @return bool - */ - public function hasVariables() - { - return ! is_null($this->variables); - } - - /** - * Get the scheduled date as a Carbon instance. - * - * @return Carbon - */ - public function getScheduledDateAsCarbon() - { - 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 - */ - public function getSendDate() - { - return $this->sent_at; - } - - /** - * Get the send error. - * - * @return string - */ - public function getError() - { - return $this->error; - } - - /** - * Determine if the e-mail should be sent with custom from values. - * - * @return bool - */ - public function hasFrom() - { - 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() - { - return strlen($this->getOriginal('cc')) > 0; - } - - /** - * Determine if the e-mail should be sent as a blind carbon copy. - * - * @return bool - */ - public function hasBcc() - { - return strlen($this->getOriginal('bcc')) > 0; - } - - /** - * Determine if the e-mail is scheduled to be sent later. - * - * @return bool - */ - public function isScheduled() - { - return ! is_null($this->getScheduledDate()); - } - - /** - * Determine if the e-mail is encrypted. - * - * @return bool - */ - public function isEncrypted() - { - return (bool) $this->getOriginal('encrypted'); - } - - /** - * Determine if the e-mail is sent. - * - * @return bool - */ - public function isSent() + public function isSent(): bool { return ! is_null($this->sent_at); } - /** - * Determine if the e-mail failed to be sent. - * - * @return bool - */ - public function hasFailed() + public function hasFailed(): bool { return $this->failed == 1; } - /** - * Mark the e-mail as sending. - * - * @return void - */ - public function markAsSending() + 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() + 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) + 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() + public function send(): void { (new Sender)->send($this); } - /** - * Retry sending the e-mail. - * - * @return void - */ - public function retry() + public static function pruneWhen(Closure $closure): void { - $retry = $this->replicate(); + static::$pruneQuery = $closure; + } + + public function prunable(): Builder + { + if (static::$pruneQuery) { + return (static::$pruneQuery)($this); + } - $retry->fill( - [ - 'id' => null, - 'attempts' => 0, - 'sending' => 0, - 'failed' => 0, - 'error' => null, - 'sent_at' => null, - 'delivered_at' => null, - ] - ); + return $this->where('created_at', '<', now()->subMonths(6)); + } - $retry->save(); + public function model(): MorphTo + { + return $this->morphTo(); } } diff --git a/src/EmailComposer.php b/src/EmailComposer.php index 502a443..67efc25 100644 --- a/src/EmailComposer.php +++ b/src/EmailComposer.php @@ -1,264 +1,187 @@ email = $email; - } + public ?Mailable $mailable = null; + + public ?Envelope $envelope = null; + + public ?Content $content = null; + + public ?array $attachments = null; + + public ?string $locale = null; - /** - * Get the e-mail that is being composed. - * - * @return Email - */ - public function getEmail() + public ?Model $model = null; + + public ?bool $queued = null; + + 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 static - */ - public function setData($key, $value) + 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($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($key) - { - return isset($this->data[$key]); + return $this; } - /** - * Set the e-mail label. - * - * @param string $label - * @return static - */ - public function label($label) + 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 array $address - * @param array $name - * @return static - */ - public function from($address = null, $name = null) - { - return $this->setData('from', compact('address', 'name')); - } + return $this; + } - /** - * Set the e-mail recipient(s). - * - * @param string|array $recipient - * @return static - */ - public function recipient($recipient) - { - return $this->setData('recipient', $recipient); - } + $this->attachments = $attachments; - /** - * Define the carbon-copy address(es). - * - * @param string|array $cc - * @return static - */ - public function cc($cc) - { - return $this->setData('cc', $cc); + return $this; } - /** - * Define the blind carbon-copy address(es). - * - * @param string|array $bcc - * @return static - */ - public function bcc($bcc) + 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); - /** - * Set the e-mail subject. - * - * @param string $subject - * @return static - */ - public function subject($subject) - { - return $this->setData('subject', $subject); + $email = method_exists($user, 'preferredEmailAddress') + ? $user->preferredEmailAddress() + : $user->email; + + if ($user instanceof HasLocalePreference) { + $this->locale = $user->preferredLocale(); + } + + return $envelope->to($email, $name); + }); } - /** - * Set the e-mail view. - * - * @param string $view - * @return static - */ - public function view($view) + public function model(Model $model) { - return $this->setData('view', $view); + $this->model = $model; + + return $this; } - /** - * Set the e-mail variables. - * - * @param array $variables - * @return EmailComposer - */ - public function variables($variables) + public function label(string $label): self { - return $this->setData('variables', $variables); + $this->email->label = $label; + + return $this; } - /** - * Schedule the e-mail. - * - * @param mixed $scheduledAt - * @return Email - */ - public function schedule($scheduledAt) + public function later($scheduledAt): Email { - return $this->later($scheduledAt); + $this->email->scheduled_at = Carbon::parse($scheduledAt); + + return $this->send(); } - /** - * Schedule the e-mail. - * - * @param mixed $scheduledAt - * @return Email - */ - public function later($scheduledAt) + public function queue(?string $connection = null, ?string $queue = null, $delay = null, ?string $jobClass = null): Email { - $this->setData('scheduled_at', $scheduledAt); + $connection = $connection ?: config('queue.default'); + $queue = $queue ?: 'default'; + + $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 static - */ - public function mailable(Mailable $mailable) + 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 static - */ - public function attach($file, $options = []) + public function send(): Email { - $attachments = $this->hasData('attachments') ? $this->getData('attachments') : []; + if ($this->envelope && $this->content) { + (new MailableReader)->read($this); + } - $attachments[] = compact('file', 'options'); + if (! is_array($this->email->from)) { + $this->email->from = []; + } - return $this->setData('attachments', $attachments); - } + $this->email->from = [ + 'name' => $this->email->from['name'] ?? config('mail.from.name'), + 'address' => $this->email->from['address'] ?? config('mail.from.address'), + ]; - /** - * Attach in-memory data as an attachment. - * - * @param string $data - * @param string $name - * @param array $options - * @return $this - */ - public function attachData($data, $name, array $options = []) - { - $attachments = $this->hasData('rawAttachments') ? $this->getData('rawAttachments') : []; + $this->email->save(); - $attachments[] = compact('data', 'name', 'options'); + $this->email->refresh(); - return $this->setData('rawAttachments', $attachments); - } + if (Config::sendImmediately()) { + $this->email->send(); - /** - * Send the e-mail. - * - * @return Email - */ - public function send() - { - (new Validator)->validate($this); + return $this->email; + } - (new Preparer)->prepare($this); + if ($this->queued) { + $job = $this->jobClass ?: SendEmailJob::class; - if (Config::encryptEmails()) { - (new Encrypter)->encrypt($this); - } + dispatch(new $job($this->email)) + ->onConnection($this->connection) + ->onQueue($this->queue) + ->delay($this->delay); - $this->email->save(); + return $this->email; + } - return $this->email->fresh(); + return $this->email; } } diff --git a/src/Encrypter.php b/src/Encrypter.php deleted file mode 100644 index ee20a8e..0000000 --- a/src/Encrypter.php +++ /dev/null @@ -1,112 +0,0 @@ -setEncrypted($composer); - - $this->encryptRecipients($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) - { - $composer->getEmail()->setAttribute('encrypted', 1); - } - - /** - * Encrypt the e-mail addresses of the recipients. - * - * @param EmailComposer $composer - */ - private function encryptRecipients(EmailComposer $composer) - { - $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) - { - $email = $composer->getEmail(); - - $email->fill([ - 'from' => encrypt($email->from), - ]); - } - - /** - * Encrypt the e-mail subject. - * - * @param EmailComposer $composer - */ - private function encryptSubject(EmailComposer $composer) - { - $email = $composer->getEmail(); - - $email->fill([ - 'subject' => encrypt($email->subject), - ]); - } - - /** - * Encrypt the e-mail variables. - * - * @param EmailComposer $composer - */ - private function encryptVariables(EmailComposer $composer) - { - 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) - { - $email = $composer->getEmail(); - - $email->fill([ - 'body' => encrypt($email->body), - ]); - } -} diff --git a/src/HasEncryptedAttributes.php b/src/HasEncryptedAttributes.php deleted file mode 100644 index d461db0..0000000 --- a/src/HasEncryptedAttributes.php +++ /dev/null @@ -1,66 +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; - } - } - - return $value; - } -} diff --git a/src/LaravelDatabaseEmailsServiceProvider.php b/src/LaravelDatabaseEmailsServiceProvider.php index 80cbb29..30a9ea4 100644 --- a/src/LaravelDatabaseEmailsServiceProvider.php +++ b/src/LaravelDatabaseEmailsServiceProvider.php @@ -1,6 +1,8 @@ bootConfig(); + $this->bootDatabase(); + } + + /** + * Boot the config for the package. + */ + private function bootConfig(): void + { + $baseDir = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR; + $configDir = $baseDir.'config'.DIRECTORY_SEPARATOR; $this->publishes([ - $configDir . 'laravel-database-emails.php' => config_path('laravel-database-emails.php'), - ]); + $configDir.'laravel-database-emails.php' => config_path('laravel-database-emails.php'), + ], 'database-emails-config'); + } + + /** + * Boot the database for the package. + */ + private function bootDatabase(): void + { + $baseDir = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR; + $migrationsDir = $baseDir.'database'.DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR; - $this->loadMigrationsFrom([$migrationsDir]); + $this->publishes([ + $migrationsDir => "{$this->app->databasePath()}/migrations", + ], 'database-emails-migrations'); } /** * Register the service provider. - * - * @return void */ - public function register() + public function register(): void { $this->commands([ SendEmailsCommand::class, - RetryFailedEmailsCommand::class, - ResendEmailsCommand::class, ]); } } diff --git a/src/MailableReader.php b/src/MailableReader.php index 90f649b..fdbfe1e 100644 --- a/src/MailableReader.php +++ b/src/MailableReader.php @@ -1,19 +1,62 @@ 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); @@ -22,125 +65,139 @@ public function read(EmailComposer $composer) $this->readBcc($composer); + $this->readReplyTo($composer); + $this->readSubject($composer); $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) + 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) + 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, + ]; + + return; + } - $composer->recipient($to); + $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) + private function readFrom(EmailComposer $composer): void { - $from = reset($composer->getData('mailable')->from); - - $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) + 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) + private function readBcc(EmailComposer $composer): void { - $bcc = $this->convertMailableAddresses( - $composer->getData('mailable')->bcc - ); + $composer->email->bcc = $this->prepareAddressForDatabaseStorage( + $composer->mailable->bcc); + } - $composer->bcc($bcc); + /** + * Read the mailable reply-to to the email composer. + */ + private function readReplyTo(EmailComposer $composer): void + { + $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) + 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) + private function readBody(EmailComposer $composer): void { - if (app()->version() < '5.5') { - throw new Exception('Mailables cannot be read by Laravel 5.4 and below. Sorry.'); - } + /** @var Mailable $mailable */ + $mailable = $composer->mailable; + + $composer->email->view = $mailable->view; + $composer->email->variables = Arr::except($mailable->buildViewData(), [ + '__laravel_mailable', + ]); - $composer->setData('view', ''); + $localeToUse = $composer->locale ?? app()->currentLocale(); - $composer->setData('body', $composer->getData('mailable')->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) + 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/MessageSent.php b/src/MessageSent.php new file mode 100644 index 0000000..3ca6e6e --- /dev/null +++ b/src/MessageSent.php @@ -0,0 +1,18 @@ +message = $message; + } +} diff --git a/src/Preparer.php b/src/Preparer.php deleted file mode 100644 index 793e8fa..0000000 --- a/src/Preparer.php +++ /dev/null @@ -1,224 +0,0 @@ -prepareLabel($composer); - - $this->prepareRecipient($composer); - - $this->prepareFrom($composer); - - $this->prepareCc($composer); - - $this->prepareBcc($composer); - - $this->prepareSubject($composer); - - $this->prepareView($composer); - - $this->prepareVariables($composer); - - $this->prepareBody($composer); - - $this->prepareAttachments($composer); - - $this->prepareScheduled($composer); - } - - /** - * Prepare the label for database storage. - * - * @param EmailComposer $composer - */ - private function prepareLabel(EmailComposer $composer) - { - 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) - { - 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) - { - $composer->getEmail()->fill([ - 'from' => json_encode($composer->getData('from', '')), - ]); - } - - /** - * Prepare the carbon copies for database storage. - * - * @param EmailComposer $composer - */ - private function prepareCc(EmailComposer $composer) - { - 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) - { - if (Config::testing()) { - $composer->setData('bcc', []); - } - - $composer->getEmail()->fill([ - 'bcc' => json_encode($composer->getData('bcc', [])), - ]); - } - - /** - * Prepare the subject for database storage. - * - * @param EmailComposer $composer - */ - private function prepareSubject(EmailComposer $composer) - { - $composer->getEmail()->fill([ - 'subject' => $composer->getData('subject'), - ]); - } - - /** - * Prepare the view for database storage. - * - * @param EmailComposer $composer - */ - private function prepareView(EmailComposer $composer) - { - $composer->getEmail()->fill([ - 'view' => $composer->getData('view'), - ]); - } - - /** - * Prepare the variables for database storage. - * - * @param EmailComposer $composer - */ - private function prepareVariables(EmailComposer $composer) - { - 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) - { - // 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) - { - $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' => json_encode($attachments), - ]); - } - - /** - * Prepare the scheduled date for database storage. - * - * @param EmailComposer $composer - */ - private function prepareScheduled(EmailComposer $composer) - { - 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(), - ]); - } -} diff --git a/src/ResendEmailsCommand.php b/src/ResendEmailsCommand.php deleted file mode 100644 index 4ae8f8c..0000000 --- a/src/ResendEmailsCommand.php +++ /dev/null @@ -1,20 +0,0 @@ -store = $store; - } - - /** - * Execute the console command. - * - * @return void - */ - public function handle() - { - if (get_class($this) === self::class) { - $this->warn('This command is deprecated, please use email:resend instead'); - } - - $emails = $this->store->getFailed( - $this->argument('id') - ); - - if ($emails->isEmpty()) { - $this->line('There is nothing to reset.'); - - return; - } - - foreach ($emails as $email) { - $email->retry(); - } - - $this->info('Reset ' . $emails->count() . ' ' . ngettext('e-mail', 'e-mails', $emails->count()) . '!'); - } - - /** - * Execute the console command (backwards compatibility for Laravel 5.4 and below). - * - * @return void - */ - public function fire() - { - $this->handle(); - } -} diff --git a/src/SendEmailJob.php b/src/SendEmailJob.php new file mode 100644 index 0000000..a865048 --- /dev/null +++ b/src/SendEmailJob.php @@ -0,0 +1,31 @@ +email = $email; + } + + public function handle(): void + { + $this->email->send(); + } +} diff --git a/src/SendEmailsCommand.php b/src/SendEmailsCommand.php index 9588ab1..71817ee 100644 --- a/src/SendEmailsCommand.php +++ b/src/SendEmailsCommand.php @@ -1,111 +1,45 @@ store = $store; - } - - /** - * Execute the console command. - * - * @return void - */ - public function handle() + public function handle(Store $store): void { - set_time_limit($this->option('timeout')); - - $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); - - set_time_limit(0); - } - - /** - * Execute the console command (backwards compatibility for Laravel 5.4 and below). - * - * @return void - */ - public function fire() - { - $this->handle(); - } - - /** - * Output a table with the cronjob result. - * - * @param Collection $emails - * @return void - */ - protected function result($emails) - { - $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 672dd66..736c830 100644 --- a/src/Sender.php +++ b/src/Sender.php @@ -1,18 +1,16 @@ isSent()) { return; @@ -20,45 +18,43 @@ public function send(Email $email) $email->markAsSending(); - $this->getMailerInstance()->send([], [], function (Message $message) use ($email) { + $sentMessage = Mail::send([], [], function (Message $message) use ($email) { $this->buildMessage($message, $email); }); - $email->markAsSent(); - } + event(new MessageSent($sentMessage)); - /** - * Get the instance of the Laravel mailer. - * - * @return Mailer - */ - private function getMailerInstance() - { - return app('mailer'); + $email->markAsSent(); } - /** - * Build the e-mail message. - * - * @param Message $message - * @param Email $email - */ - private function buildMessage(Message $message, Email $email) + private function buildMessage(Message $message, Email $email): void { - $message->to($email->getRecipient()) - ->cc($email->hasCc() ? $email->getCc() : []) - ->bcc($email->hasBcc() ? $email->getBcc() : []) - ->subject($email->getSubject()) - ->from($email->getFromAddress(), $email->getFromName()) - ->setBody($email->getBody(), 'text/html'); - - $attachmentMap = [ - 'attachment' => 'attach', - 'rawAttachment' => 'attachData', - ]; - - foreach ((array) $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 new file mode 100644 index 0000000..9e075da --- /dev/null +++ b/src/SentMessage.php @@ -0,0 +1,65 @@ +getFrom() as $address) { + $sentMessage->from[$address->getAddress()] = $address->getName(); + } + + foreach ($email->getTo() as $address) { + $sentMessage->to[$address->getAddress()] = $address->getName(); + } + + foreach ($email->getCc() as $address) { + $sentMessage->cc[$address->getAddress()] = $address->getName(); + } + + foreach ($email->getBcc() as $address) { + $sentMessage->bcc[$address->getAddress()] = $address->getName(); + } + + foreach ($email->getReplyTo() as $address) { + $sentMessage->replyTo[$address->getAddress()] = $address->getName(); + } + + $sentMessage->subject = $email->getSubject(); + $sentMessage->body = $email->getHtmlBody(); + $sentMessage->attachments = array_map(function (DataPart $dataPart) { + return [ + 'body' => $dataPart->getBody(), + 'disposition' => $dataPart->asDebugString(), + ]; + }, $email->getAttachments()); + + return $sentMessage; + } +} diff --git a/src/Store.php b/src/Store.php index 9fc73c0..653c5ff 100644 --- a/src/Store.php +++ b/src/Store.php @@ -1,9 +1,12 @@ whereNull('deleted_at') ->whereNull('sent_at') + ->whereNull('queued_at') ->where(function ($query) { $query->whereNull('scheduled_at') ->orWhere('scheduled_at', '<=', Carbon::now()->toDateTimeString()); @@ -27,27 +31,6 @@ public function getQueue() ->where('attempts', '<', Config::maxAttemptCount()) ->orderBy('created_at', 'asc') ->limit(Config::cronjobEmailLimit()) - ->get(); - } - - /** - * Get all e-mails that failed to be sent. - * - * @param int $id - * @return Collection|Email[] - */ - public function getFailed($id = null) - { - $query = new Email; - - return $query - ->when($id, function ($query) use ($id) { - $query->where('id', '=', $id); - }) - ->where('failed', '=', 1) - ->where('attempts', '>=', Config::maxAttemptCount()) - ->whereNull('sent_at') - ->whereNull('deleted_at') - ->get(); + ->cursor(); } } diff --git a/src/Validator.php b/src/Validator.php deleted file mode 100644 index 2505ff1..0000000 --- a/src/Validator.php +++ /dev/null @@ -1,190 +0,0 @@ -validateLabel($composer); - - $this->validateRecipient($composer); - - $this->validateCc($composer); - - $this->validateBcc($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) - { - 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) - { - if (! $composer->hasData('recipient')) { - throw new InvalidArgumentException('No recipient specified'); - } - - $recipients = (array) $composer->getData('recipient'); - - 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) - { - 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) - { - 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 e-mail subject. - * - * @param EmailComposer $composer - * @throws InvalidArgumentException - */ - private function validateSubject(EmailComposer $composer) - { - 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) - { - 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) - { - 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) - { - 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..f823bc9 --- /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('tests::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('tests::welcome')) + ->send(); + + $this->assertNull($email->model_type); + $this->assertNull($email->model_id); + } +} diff --git a/tests/ConfigCacheTest.php b/tests/ConfigCacheTest.php new file mode 100644 index 0000000..5c04075 --- /dev/null +++ b/tests/ConfigCacheTest.php @@ -0,0 +1,27 @@ +fail('Configuration file cannot be serialized'); + } else { + $this->assertTrue(true); + } + } +} diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php new file mode 100644 index 0000000..27bbba0 --- /dev/null +++ b/tests/ConfigTest.php @@ -0,0 +1,49 @@ +assertEquals(3, Config::maxAttemptCount()); + + $this->app['config']->set('database-emails.attempts', 5); + + $this->assertEquals(5, Config::maxAttemptCount()); + } + + #[Test] + public function test_testing() + { + $this->assertFalse(Config::testing()); + + $this->app['config']->set('database-emails.testing.enabled', true); + + $this->assertTrue(Config::testing()); + } + + #[Test] + public function test_test_email_address() + { + $this->assertEquals('test@email.com', Config::testEmailAddress()); + + $this->app['config']->set('database-emails.testing.email', 'test+update@email.com'); + + $this->assertEquals('test+update@email.com', Config::testEmailAddress()); + } + + #[Test] + public function test_cronjob_email_limit() + { + $this->assertEquals(20, Config::cronjobEmailLimit()); + + $this->app['config']->set('database-emails.limit', 15); + + $this->assertEquals(15, Config::cronjobEmailLimit()); + } +} diff --git a/tests/DatabaseInteractionTest.php b/tests/DatabaseInteractionTest.php index 2f65045..11fdb6d 100644 --- a/tests/DatabaseInteractionTest.php +++ b/tests/DatabaseInteractionTest.php @@ -2,33 +2,36 @@ namespace Tests; -use Dompdf\Dompdf; +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,61 +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' => [ + 'john@doe.com', + ], + ]); + + $this->assertEquals(['john@doe.com' => null], $email->reply_to); + } + + #[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(date('Y-m-d H:i:s', strtotime('+2 weeks')), $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']]); @@ -98,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(); @@ -144,80 +140,73 @@ 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() { - $scheduledFor = date('Y-m-d H:i:s', strtotime('+2 weeks')); + Carbon::setTestNow(Carbon::now()); + + $scheduledFor = date('Y-m-d H:i:s', Carbon::now()->addWeek(2)->timestamp); $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() { - $pdf = new Dompdf; - $pdf->loadHtml('Hello CI!'); - $pdf->setPaper('A4', 'landscape'); + $this->expectExceptionMessage('Raw attachments are not supported in the database email driver.'); - $email = $this->composeEmail() - ->attachData($pdf->outputHtml(), '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($pdf->outputHtml(), $email->getAttachments()[0]['attachment']['data']); } } diff --git a/tests/EncryptionTest.php b/tests/EncryptionTest.php deleted file mode 100644 index c4087e2..0000000 --- a/tests/EncryptionTest.php +++ /dev/null @@ -1,100 +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->getOriginal('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->getOriginal('cc'))); - $this->assertEquals($bcc, decrypt($email->getOriginal('bcc'))); - - $this->assertEquals($cc, $email->getCc()); - $this->assertEquals($bcc, $email->getBcc()); - } - - /** @test */ - public function the_subject_should_be_encrypted_and_decrypted() - { - $email = $this->sendEmail(['subject' => 'test subject']); - - $this->assertEquals('test subject', decrypt($email->getOriginal('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->getOriginal('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->getOriginal('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->getOriginal('from'))); - $this->assertEquals($expect, $email->getFrom()); - } -} diff --git a/tests/EnvelopeTest.php b/tests/EnvelopeTest.php new file mode 100644 index 0000000..bc96437 --- /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('tests::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('tests::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('tests::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 f60ff8a..bf98627 100644 --- a/tests/MailableReaderTest.php +++ b/tests/MailableReaderTest.php @@ -3,133 +3,153 @@ namespace Tests; use Illuminate\Mail\Mailable; -use Buildcode\LaravelDatabaseEmails\Email; +use Illuminate\Mail\Mailables\Address; +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 { - /** @test */ + private function mailable(): Mailable + { + return new TestMailable; + } + + #[Test] public function it_extracts_the_recipient() { $composer = Email::compose() - ->mailable(new TestMailable()); + ->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( - (new TestMailable())->to(['jane@doe.com']) + $this->mailable()->to(['jane@doe.com']) ); - $this->assertEquals(['john@doe.com', '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(new TestMailable()); + $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(new TestMailable()); + $composer = Email::compose()->mailable($this->mailable()); + + $this->assertEquals(['john+bcc@doe.com' => null, 'john+bcc2@doe.com' => null], $composer->email->bcc); + } + + #[Test] + public function it_extracts_reply_to_addresses() + { + $composer = Email::compose()->mailable($this->mailable()); - $this->assertEquals(['john+bcc@doe.com', 'john+bcc2@doe.com'], $composer->getData('bcc')); + $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(new TestMailable()); + $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(new TestMailable()); + $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(new TestMailable())->send(); + $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( - (new TestMailable()) + ($this->mailable()) ->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( - (new TestMailable()) + ($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('Laravel', $email->from['name']); $email = Email::compose()->mailable( - (new TestMailable()) - ->from(null, 'Marick') + ($this->mailable()) + ->from('marick@dolphiq.nl', 'Marick') )->send(); - $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 { - /** - * Create a new message instance. - * - * @return void - */ - public function __construct() + public function content(): Content + { + $content = new Content( + 'tests::dummy' + ); + + $content->with('name', 'John Doe'); + + return $content; + } + + public function envelope(): Envelope { - $this->to('john@doe.com') - ->cc(['john+cc@doe.com', 'john+cc2@doe.com']) - ->bcc(['john+bcc@doe.com', 'john+bcc2@doe.com']) - ->subject('Your order has shipped!') - ->attach(__DIR__ . '/files/pdf-sample.pdf', [ - 'mime' => 'application/pdf', - ]) - ->attachData('

Thanks for your oder

', 'order.html'); + return new Envelope( + null, + [ + new Address('john@doe.com', 'John Doe'), + ], + ['john+cc@doe.com', 'john+cc2@doe.com'], + ['john+bcc@doe.com', 'john+bcc2@doe.com'], + ['replyto@example.com', new Address('replyto2@example.com')], + 'Your order has shipped!' + ); } - /** - * Build the message. - * - * @return $this - */ - public function build() + public function attachments(): array { - return $this->view('tests::dummy', ['name' => 'John Doe']); + return [ + 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 new file mode 100644 index 0000000..b562f35 --- /dev/null +++ b/tests/PruneTest.php @@ -0,0 +1,49 @@ +sendEmail(); + + 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'); + + // Ensure the email object has to be passed manually, otherwise we are acidentally + // deleting everyone's e-mails... + $this->artisan('model:prune'); + $this->assertInstanceOf(Email::class, $email->fresh()); + + // Now test with it passed... then it should definitely be deleted. + $this->artisan('model:prune', ['--model' => [Email::class]]); + $this->assertNull($email->fresh()); + } + + #[Test] + public function can_change_when_emails_are_pruned() + { + Email::pruneWhen(function (Email $email) { + return $email->where('created_at', '<', now()->subMonths(3)); + }); + + $email = $this->sendEmail(); + + 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'); + $this->artisan('model:prune', ['--model' => [Email::class]]); + $this->assertNull($email->fresh()); + } +} diff --git a/tests/QueuedEmailsTest.php b/tests/QueuedEmailsTest.php new file mode 100644 index 0000000..124f57f --- /dev/null +++ b/tests/QueuedEmailsTest.php @@ -0,0 +1,119 @@ +queueEmail(); + + $this->assertEquals(0, $email->sending); + } + + #[Test] + public function queueing_an_email_will_set_the_queued_at_column() + { + Queue::fake(); + + $email = $this->queueEmail(); + + $this->assertNotNull($email->queued_at); + } + + #[Test] + public function queueing_an_email_will_dispatch_a_job() + { + Queue::fake(); + + $email = $this->queueEmail(); + + Queue::assertPushed(SendEmailJob::class, function (SendEmailJob $job) use ($email) { + return $job->email->id === $email->id; + }); + } + + #[Test] + public function emails_can_be_queued_on_a_specific_connection() + { + Queue::fake(); + + $this->queueEmail('some-connection'); + + Queue::assertPushed(SendEmailJob::class, function (SendEmailJob $job) { + return $job->connection === 'some-connection'; + }); + } + + #[Test] + public function emails_can_be_queued_on_a_specific_queue() + { + Queue::fake(); + + $this->queueEmail('default', 'some-queue'); + + Queue::assertPushed(SendEmailJob::class, function (SendEmailJob $job) { + return $job->queue === 'some-queue'; + }); + } + + #[Test] + public function emails_can_be_queued_with_a_delay() + { + Queue::fake(); + + $delay = now()->addMinutes(6); + + $this->queueEmail(null, null, $delay); + + Queue::assertPushed(SendEmailJob::class, function (SendEmailJob $job) use ($delay) { + return $job->delay->getTimestamp() === $delay->timestamp; + }); + } + + #[Test] + public function the_send_email_job_will_call_send_on_the_email_instance() + { + Queue::fake(); + + $email = $this->queueEmail('default', 'some-queue'); + + $job = new SendEmailJob($email); + + Mail::shouldReceive('send')->once(); + + $job->handle(); + } + + #[Test] + public function the_mail_will_be_marked_as_sent_when_job_is_finished() + { + Queue::fake(); + + $email = $this->queueEmail('default', 'some-queue'); + + $job = new SendEmailJob($email); + $job->handle(); + + $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/RetryFailedEmailsCommandTest.php b/tests/RetryFailedEmailsCommandTest.php deleted file mode 100644 index 794eda1..0000000 --- a/tests/RetryFailedEmailsCommandTest.php +++ /dev/null @@ -1,67 +0,0 @@ -app['config']['laravel-database-emails.attempts'] = 3; - } - - /** @test */ - public function an_email_cannot_be_reset_if_the_max_attempt_count_has_not_been_reached() - { - $this->app['config']['mail.driver'] = 'does-not-exist'; - - $this->sendEmail(); - - $this->artisan('email:send'); - - $this->assertEquals(1, DB::table('emails')->count()); - - $this->artisan('email:resend'); - - $this->assertEquals(1, DB::table('emails')->count()); - - // try 2 more times, reaching 3 attempts and thus failing and able to retry - $this->artisan('email:send'); - $this->artisan('email:send'); - $this->artisan('email:resend'); - - $this->assertEquals(2, DB::table('emails')->count()); - } - - /** @test */ - public function a_single_email_can_be_resent() - { - $emailA = $this->sendEmail(); - $emailB = $this->sendEmail(); - - // simulate emailB being failed... - $emailB->update(['failed' => 1, 'attempts' => 3]); - - $this->artisan('email:resend', ['id' => 2]); - - $this->assertEquals(3, DB::table('emails')->count()); - } - - /** @test */ - public function email_retry_is_deprecated() - { - $deprecated = 'This command is deprecated'; - - $this->artisan('email:retry'); - - $this->assertContains($deprecated, Artisan::output()); - - $this->artisan('email:resend'); - - $this->assertNotContains($deprecated, Artisan::output()); - } -} diff --git a/tests/SendEmailsCommandTest.php b/tests/SendEmailsCommandTest.php index 1d3e3e0..ae447ed 100644 --- a/tests/SendEmailsCommandTest.php +++ b/tests/SendEmailsCommandTest.php @@ -4,91 +4,105 @@ use Carbon\Carbon; use Illuminate\Support\Facades\DB; -use Buildcode\LaravelDatabaseEmails\Store; +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->assertEquals($firstSend = date('Y-m-d H:i:s'), $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 */ - public function if_an_email_fails_to_be_sent_it_should_be_logged_in_the_database() + #[Test] + public function an_email_should_not_be_sent_if_it_is_queued() { - $this->app['config']['mail.driver'] = 'does-not-exist'; + Queue::fake(); + + $email = $this->queueEmail(); + + $this->artisan('email:send'); + $this->assertNull($email->fresh()->sent_at); + } + + #[Test] + public function if_an_email_fails_to_be_sent_it_should_be_logged_in_the_database() + { $email = $this->sendEmail(); + $email->update(['recipient' => ['asdf' => null]]); + $this->artisan('email:send'); $this->assertTrue($email->fresh()->hasFailed()); - $this->assertContains('Driver [does-not-exist] not supported.', $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'; @@ -103,37 +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()); - } - - /** @test */ - public function the_command_will_be_stopped_after_the_timeout() - { - $this->assertEquals(0, ini_get('max_execution_time')); - - $this->artisan('email:send'); - - $this->assertEquals(300, ini_get('max_execution_time')); - - $this->artisan('email:send', ['--timeout' => 60]); - - $this->assertEquals(60, ini_get('max_execution_time')); + $this->assertFalse($email->fresh()->failed); + $this->assertEmpty($email->fresh()->error); } } diff --git a/tests/SenderTest.php b/tests/SenderTest.php index 0bd1f9e..4f08d2a 100644 --- a/tests/SenderTest.php +++ b/tests/SenderTest.php @@ -2,34 +2,43 @@ namespace Tests; -use Dompdf\Dompdf; -use Swift_Events_SendEvent; +use Illuminate\Mail\Mailables\Address; +use Illuminate\Support\Facades\Event; 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() + protected function setUp(): void { parent::setUp(); - 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(); - Mail::shouldReceive('send') - ->once(); + Mail::shouldReceive('send')->once(); $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'); @@ -39,7 +48,7 @@ public function the_email_has_a_correct_from_email_and_from_name() $this->artisan('email:send'); - $from = reset($this->sent)->getMessage()->getFrom(); + $from = reset($this->sent)->from; $this->assertEquals('testfromaddress@gmail.com', key($from)); $this->assertEquals('From CI test', $from[key($from)]); @@ -47,146 +56,193 @@ 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)->getMessage()->getFrom(); + $from = reset($this->sent)->from; $this->assertEquals('marick@dolphiq.nl', key($from)); $this->assertEquals('Marick', $from[key($from)]); // 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)->getMessage()->getFrom(); + $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)->getMessage()->getFrom(); - $this->assertEquals(config('mail.from.address'), key($from)); - $this->assertEquals('Marick', $from[key($from)]); + $this->assertEquals('From CI test', $from[key($from)]); } - /** @test */ + #[Test] public function it_sends_emails_to_the_correct_recipients() { $this->sendEmail(['recipient' => 'john@doe.com']); $this->artisan('email:send'); - $to = reset($this->sent)->getMessage()->getTo(); + $to = reset($this->sent)->to; $this->assertCount(1, $to); $this->assertArrayHasKey('john@doe.com', $to); $this->sent = []; $this->sendEmail(['recipient' => ['john@doe.com', 'john+2@doe.com']]); $this->artisan('email:send'); - $to = reset($this->sent)->getMessage()->getTo(); + $to = reset($this->sent)->to; $this->assertCount(2, $to); $this->assertArrayHasKey('john@doe.com', $to); $this->assertArrayHasKey('john+2@doe.com', $to); } - /** @test */ + #[Test] public function it_adds_the_cc_addresses() { $this->sendEmail(['cc' => 'cc@test.com']); $this->artisan('email:send'); - $cc = reset($this->sent)->getMessage()->getCc(); + $cc = reset($this->sent)->cc; $this->assertCount(1, $cc); $this->assertArrayHasKey('cc@test.com', $cc); $this->sent = []; $this->sendEmail(['cc' => ['cc@test.com', 'cc+2@test.com']]); $this->artisan('email:send'); - $cc = reset($this->sent)->getMessage()->getCc(); + $cc = reset($this->sent)->cc; $this->assertCount(2, $cc); $this->assertArrayHasKey('cc@test.com', $cc); $this->assertArrayHasKey('cc+2@test.com', $cc); } - /** @test */ + #[Test] public function it_adds_the_bcc_addresses() { $this->sendEmail(['bcc' => 'bcc@test.com']); $this->artisan('email:send'); - $bcc = reset($this->sent)->getMessage()->getBcc(); + $bcc = reset($this->sent)->bcc; $this->assertCount(1, $bcc); $this->assertArrayHasKey('bcc@test.com', $bcc); $this->sent = []; $this->sendEmail(['bcc' => ['bcc@test.com', 'bcc+2@test.com']]); $this->artisan('email:send'); - $bcc = reset($this->sent)->getMessage()->getBcc(); + $bcc = reset($this->sent)->bcc; $this->assertCount(2, $bcc); $this->assertArrayHasKey('bcc@test.com', $bcc); $this->assertArrayHasKey('bcc+2@test.com', $bcc); } - /** @test */ + #[Test] public function the_email_has_the_correct_subject() { $this->sendEmail(['subject' => 'Hello World']); $this->artisan('email:send'); - $subject = reset($this->sent)->getMessage()->getSubject(); + $subject = reset($this->sent)->subject; $this->assertEquals('Hello World', $subject); } - /** @test */ + #[Test] public function the_email_has_the_correct_body() { $this->sendEmail(['variables' => ['name' => 'John Doe']]); $this->artisan('email:send'); - $body = reset($this->sent)->getMessage()->getBody(); - $this->assertEquals(view('tests::dummy', ['name' => 'John Doe']), $body); + $body = reset($this->sent)->body; + $this->assertEquals((string) view('tests::dummy', ['name' => 'John Doe']), $body); $this->sent = []; $this->sendEmail(['variables' => []]); $this->artisan('email:send'); - $body = reset($this->sent)->getMessage()->getBody(); + $body = reset($this->sent)->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)->getMessage()->getChildren(); - $attachment = reset($attachments); + $attachments = reset($this->sent)->attachments; - $this->assertCount(1, $attachments); - $this->assertEquals('attachment; filename=pdf-sample.pdf', $attachment->getHeaders()->get('content-disposition')->getFieldBody()); - $this->assertEquals('application/pdf', $attachment->getContentType()); + $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() { - $pdf = new Dompdf; - $pdf->loadHtml('Hello CI!'); - $pdf->setPaper('A4'); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Raw attachments are not supported in the database email driver.'); $this->composeEmail() - ->attachData($pdf->outputHtml(), 'hello-ci.pdf', [ - 'mime' => 'application/pdf', + ->attachments([ + Attachment::fromData(fn () => 'test', 'test.txt'), ]) ->send(); + } + + #[Test] + public function emails_can_be_sent_immediately() + { + $this->app['config']->set('database-emails.immediately', false); + $this->sendEmail(); + $this->assertCount(0, $this->sent); + Email::truncate(); + + $this->app['config']->set('database-emails.immediately', true); + $this->sendEmail(); + $this->assertCount(1, $this->sent); + $this->artisan('email:send'); + $this->assertCount(1, $this->sent); + } - $attachments = reset($this->sent)->getMessage()->getChildren(); - $attachment = reset($attachments); + #[Test] + public function it_adds_the_reply_to_addresses() + { + $this->sendEmail(['reply_to' => 'replyto@test.com']); + $this->artisan('email:send'); + $replyTo = reset($this->sent)->replyTo; + $this->assertCount(1, $replyTo); + $this->assertArrayHasKey('replyto@test.com', $replyTo); - $this->assertCount(1, $attachments); - $this->assertEquals('attachment; filename=hello-ci.pdf', $attachment->getHeaders()->get('content-disposition')->getFieldBody()); - $this->assertEquals('application/pdf', $attachment->getContentType()); - $this->assertContains('Hello CI!', $attachment->getBody()); + $this->sent = []; + $this->sendEmail(['reply_to' => ['replyto1@test.com', 'replyto2@test.com']]); + $this->artisan('email:send'); + $replyTo = reset($this->sent)->replyTo; + $this->assertCount(2, $replyTo); + $this->assertArrayHasKey('replyto1@test.com', $replyTo); + $this->assertArrayHasKey('replyto2@test.com', $replyTo); + + $this->sent = []; + $this->sendEmail([ + 'reply_to' => new Address('replyto@test.com', 'NoReplyTest'), + ]); + $this->artisan('email:send'); + $replyTo = reset($this->sent)->replyTo; + $this->assertCount(1, $replyTo); + $this->assertSame(['replyto@test.com' => 'NoReplyTest'], $replyTo); + + $this->sent = []; + $this->sendEmail([ + 'reply_to' => [ + new Address('replyto@test.com', 'NoReplyTest'), + new Address('replyto2@test.com', 'NoReplyTest2'), + ], + ]); + $this->artisan('email:send'); + $replyTo = reset($this->sent)->replyTo; + $this->assertCount(2, $replyTo); + $this->assertSame( + [ + 'replyto@test.com' => 'NoReplyTest', + 'replyto2@test.com' => 'NoReplyTest2', + ], + $replyTo + ); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index f2d5679..27e2d16 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,14 +2,21 @@ namespace Tests; -use Eloquent; -use Buildcode\LaravelDatabaseEmails\Email; +use Illuminate\Foundation\Testing\RefreshDatabase; +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; - public function setUp() + use RefreshDatabase; + use WithWorkbench; + + protected function setUp(): void { parent::setUp(); @@ -19,99 +26,99 @@ public function setUp() 1, 1.0, 'test', - new \stdClass(), + new \stdClass, (object) [], - function () { - }, + function () {}, ]; - $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); + view()->addNamespace('tests', __DIR__.'/views'); - view()->addNamespace('tests', __DIR__ . '/views'); + $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); Email::truncate(); } - /** - * Get a database connection instance. - * - * @return \Illuminate\Database\Connection - */ - protected function connection() - { - return Eloquent::getConnectionResolver()->connection(); - } - - /** - * Get a schema builder instance. - * - * @return \Illuminate\Database\Schema\Builder - */ - protected function schema() - { - return $this->connection()->getSchemaBuilder(); - } - - /** - * 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 [ - \Orchestra\Database\ConsoleServiceProvider::class, - \Buildcode\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('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'), - 'database' => getenv('CI_DB_DATABASE'), - 'username' => getenv('CI_DB_USERNAME'), - 'password' => getenv('CI_DB_PASSWORD'), - 'prefix' => '', - 'strict' => true, + 'driver' => $driver, + ...match ($driver) { + 'sqlite' => [ + 'database' => database_path('database.sqlite'), + ], + 'mysql' => [ + 'host' => '127.0.0.1', + 'port' => 3307, + 'database' => 'test', + 'username' => 'test', + 'password' => 'test', + ], + 'pgsql' => [ + 'host' => '127.0.0.1', + 'port' => 5432, + 'database' => 'test', + 'username' => 'test', + 'password' => 'test', + ], + }, ]); + + $app['config']->set('mail.driver', 'log'); + + $app['config']->set('mail.from.name', 'Laravel'); } public function createEmail($overwrite = []) { $params = array_merge([ - 'label' => 'welcome', + 'label' => 'welcome', 'recipient' => 'john@doe.com', - 'cc' => null, - 'bcc' => 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']) - ->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 = []) @@ -126,6 +133,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 = [], ?string $jobClass = null) + { + return $this->createEmail($overwrite)->queue($connection, $queue, $delay, $jobClass); } } diff --git a/tests/TestingMailEventListener.php b/tests/TestingMailEventListener.php deleted file mode 100644 index b143350..0000000 --- a/tests/TestingMailEventListener.php +++ /dev/null @@ -1,21 +0,0 @@ -test = $test; - } - - public function beforeSendPerformed($event) - { - $this->test->sent[] = $event; - } -} diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php deleted file mode 100644 index 78a26e4..0000000 --- a/tests/ValidatorTest.php +++ /dev/null @@ -1,186 +0,0 @@ -label(str_repeat('a', 256)) - ->send(); - } - - /** - * @test - * @expectedException InvalidArgumentException - * @expectedExceptionMessage No recipient specified - */ - public function a_recipient_is_required() - { - Email::compose() - ->send(); - } - - /** - * @test - * @expectedException InvalidArgumentException - * @expectedExceptionMessage E-mail address [not-a-valid-email-address] is invalid - */ - public function the_recipient_email_must_be_valid() - { - Email::compose() - ->recipient('not-a-valid-email-address') - ->send(); - } - - /** - * @test - * @expectedException InvalidArgumentException - * @expectedExceptionMessage E-mail address [not-a-valid-email-address] is invalid - */ - public function cc_must_contain_valid_email_addresses() - { - Email::compose() - ->recipient('john@doe.com') - ->cc([ - 'jane@doe.com', - 'not-a-valid-email-address', - ]) - ->send(); - } - - /** - * @test - * @expectedException InvalidArgumentException - * @expectedExceptionMessage E-mail address [not-a-valid-email-address] is invalid - */ - public function bcc_must_contain_valid_email_addresses() - { - Email::compose() - ->recipient('john@doe.com') - ->bcc([ - 'jane@doe.com', - 'not-a-valid-email-address', - ]) - ->send(); - } - - /** - * @test - * @expectedException InvalidArgumentException - * @expectedExceptionMessage No subject specified - */ - public function a_subject_is_required() - { - Email::compose() - ->recipient('john@doe.com') - ->send(); - } - - /** - * @test - * @expectedException InvalidArgumentException - * @expectedExceptionMessage No view specified - */ - public function a_view_is_required() - { - 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 (InvalidArgumentException $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/tests/views/welcome.blade.php b/tests/views/welcome.blade.php new file mode 100644 index 0000000..01f3f00 --- /dev/null +++ b/tests/views/welcome.blade.php @@ -0,0 +1 @@ +Welcome \ No newline at end of file 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 @@ +