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 @@
-
-
-
-
-
-
-
-
+[](https://github.com/stackkit/laravel-database-emails/actions/workflows/run-tests.yml)
+[](https://packagist.org/packages/stackkit/laravel-database-emails)
+[](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 @@
+