diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cd8eb86 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bb6265e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/.scrutinizer.yml export-ignore +/tests export-ignore +/.editorconfig export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..896e906 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build +composer.lock +docs +vendor +coverage +.phpunit.result.cache diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..df16b68 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,19 @@ +filter: + excluded_paths: [tests/*] + +checks: + php: + remove_extra_empty_lines: true + remove_php_closing_tag: true + remove_trailing_whitespace: true + fix_use_statements: + remove_unused: true + preserve_multiple: false + preserve_blanklines: true + order_alphabetically: true + fix_php_opening_tag: true + fix_linefeed: true + fix_line_ending: true + fix_identation_4spaces: true + fix_doc_comments: true + diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..f4d3cbc --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,4 @@ +preset: laravel + +disabled: + - single_class_element_per_statement diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..24c2d6c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: php + +php: + - 7.1 + - 7.2 + - 7.3 + +env: + matrix: + - COMPOSER_FLAGS="--prefer-lowest" + - COMPOSER_FLAGS="" + +before_script: + - travis_retry composer self-update + - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source + +script: + - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + +after_script: + - php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5a7bba5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to `laravel-test-mail` will be documented in this file + +## 1.0.0 - 201X-XX-XX + +- initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b4ae1c4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..7d7e69b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Sean White + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7e68a5 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# Laravel Test Mail + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/resohead/laravel-test-mail.svg?style=flat-square)](https://packagist.org/packages/resohead/laravel-test-mail) +[![Build Status](https://img.shields.io/travis/resohead/laravel-test-mail/master.svg?style=flat-square)](https://travis-ci.org/resohead/laravel-test-mail) +[![Quality Score](https://img.shields.io/scrutinizer/g/resohead/laravel-test-mail.svg?style=flat-square)](https://scrutinizer-ci.com/g/resohead/laravel-test-mail) +[![Total Downloads](https://img.shields.io/packagist/dt/resohead/laravel-test-mail.svg?style=flat-square)](https://packagist.org/packages/resohead/laravel-test-mail) + +A simple package to send test emails from artisan commands. + +## Installation + +You can install the package via composer: + +```bash +composer require resohead/laravel-test-mail +``` + +The package will automatically register itself. + +## Usage + +To send a test email run the following artisan command: + +``` php +php artisan mail:test +``` + +By default this will use: +- the 'from' address defined in your mail config, +- your default mail driver, +- synchronous processing + +Alternatively you have three other options in the command signature: +- set the email address, +- change the mail driver, +- enable for queuing +- change the queue connection + +Changing the mail driver and running through a queue might require the queue worker to be reset. + +``` bash +// send using the default mail driver and default queue/stack to the specified email +php artisan mail:test name@example.com --queue + +// queue using the 'log' mail driver +php artisan mail:test --driver=log + +// queue using the 'emails' queue on the default connection +php artisan mail:test --stack=emails + +// queue using the sqs queue connection, default queue and default mail driver +php artisan mail:test --connection=sqs + +// send a test mail using the SMTP driver via the emails queue on the redis connection +php artisan mail:test name@example.com --driver=smtp --connection=redis --stack=emails + +``` +> You might need to start the your queue if using the connection option, for example +``` +php artisan queue:work sqs +``` +## Alternatives + +This is a simple package designed to quickly trigger an email to check your configuration. + +If you want to check what an email looks like in the browser use the Laravel documentation to [render mailables](https://laravel.com/docs/mail#rendering-mailables) (available since Laravel 5.5). + +If you need a package to send a mailable using fake data try using [Spatie's laravel-mailable-test package](https://github.com/spatie/laravel-mailable-test). + +### Testing + +``` bash +composer test +``` + +### Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Credits + +- [Sean White](https://github.com/resohead) +- [All Contributors](../../contributors) + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ce56ab2 --- /dev/null +++ b/composer.json @@ -0,0 +1,54 @@ +{ + "name": "resohead/laravel-test-mail", + "description": "Quickly send test emails using commands in Laravel applications", + "keywords": [ + "resohead", + "laravel-test-mail" + ], + "homepage": "https://github.com/resohead/laravel-test-mail", + "license": "MIT", + "type": "library", + "authors": [ + { + "name": "Sean White", + "email": "s.white9904@gmail.com", + "role": "Developer" + } + ], + "require": { + "php": "^7.1", + "illuminate/support": "^6.0|^7.0" + }, + "require-dev": { + "orchestra/testbench": "^4.0", + "phpunit/phpunit": "^8.0" + }, + "autoload": { + "psr-4": { + "Resohead\\LaravelTestMail\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Resohead\\LaravelTestMail\\Tests\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-coverage": "vendor/bin/phpunit --coverage-html coverage" + + }, + "config": { + "sort-packages": true + }, + "extra": { + "laravel": { + "providers": [ + "Resohead\\LaravelTestMail\\LaravelTestMailServiceProvider" + ], + "aliases": { + "LaravelTestMail": "Resohead\\LaravelTestMail\\LaravelTestMailFacade" + } + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..22fe879 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + tests + + + + + src/ + + + + + + + + + + diff --git a/resources/views/emails/test.blade.php b/resources/views/emails/test.blade.php new file mode 100644 index 0000000..fa7fb99 --- /dev/null +++ b/resources/views/emails/test.blade.php @@ -0,0 +1,11 @@ +@component('mail::message') + +This is a test email. + +@component('mail::button', ['url' => config('app.url')]) +Open Website +@endcomponent + +Thanks,
+{{ config('app.name') }} +@endcomponent diff --git a/src/LaravelTestMailServiceProvider.php b/src/LaravelTestMailServiceProvider.php new file mode 100644 index 0000000..579cf0e --- /dev/null +++ b/src/LaravelTestMailServiceProvider.php @@ -0,0 +1,27 @@ +loadViewsFrom(__DIR__.'/../resources/views', 'laravel-test-mail'); + } + + /** + * Register the application services. + */ + public function register() + { + $this->commands([ + TestMailCommand::class, + ]); + } +} diff --git a/src/TestMail.php b/src/TestMail.php new file mode 100644 index 0000000..5be7d8e --- /dev/null +++ b/src/TestMail.php @@ -0,0 +1,37 @@ +recipient = $recipient; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->subject('Test email from '. config('app.name')) + ->to($this->recipient) + ->markdown('laravel-test-mail::emails.test'); + } +} diff --git a/src/TestMailCommand.php b/src/TestMailCommand.php new file mode 100644 index 0000000..8e50907 --- /dev/null +++ b/src/TestMailCommand.php @@ -0,0 +1,86 @@ +validator = $validator; + $this->config = $config; + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $recipient = $this->argument('recipient') ?? config('mail.from.address'); + $driver = $this->option('driver') ?? $this->config->get('mail.driver'); + $stack = $this->option('stack') ?: 'default'; + $connection = $this->option('connection') ?? $this->config->get('queue.default'); + + $validation = $this->validator::make([ + 'email' => $recipient, + 'driver' => $driver + ], $this->rules() + ); + + if ($validation->fails()) { + collect($validation->errors()->all())->each(function($error){ + $this->error($error); + }); + return 1; + } + + $this->config->set('mail.driver', $driver); + + $mailable = new TestMail($recipient); + + (bool) $this->option('queue') || ($this->option('stack') || $this->option('connection')) + ? Mail::queue($mailable->onConnection($connection)->onQueue($stack)) + : Mail::send($mailable); + + $this->comment("A test email (${driver}) has been sent to ${recipient}"); + } + + protected function rules() + { + return [ + 'email' => 'email', + 'driver' => 'required' + ]; + } +} diff --git a/tests/TestMailTest.php b/tests/TestMailTest.php new file mode 100644 index 0000000..e438375 --- /dev/null +++ b/tests/TestMailTest.php @@ -0,0 +1,276 @@ +defaultEmailAddress = 'recipient@example.com'; + $app['config']->set('mail.from.address', $this->defaultEmailAddress); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->command = 'mail:test'; + $this->emailAddress = 'recipient@example.com'; + + Mail::fake(); + } + + /** @test */ + public function it_can_be_sent_without_any_arguments() + { + $exitCode = Artisan::call($this->command); + $this->assertStringContainsString($this->defaultEmailAddress, Artisan::output()); + $this->assertEquals(0, $exitCode); + + Mail::assertSent(TestMail::class, function (TestMail $mail) { + $mail->build(); + $this->assertCount(1, $mail->to); + $this->assertEquals($this->emailAddress, $mail->to[0]['address']); + $this->assertCount(0, $mail->cc); + $this->assertCount(0, $mail->bcc); + + return true; + }); + } + + /** @test */ + public function it_can_be_sent_synchronously_to_an_email() + { + $exitCode = Artisan::call($this->command,[ + 'recipient' => $this->emailAddress + ]); + + $this->assertStringContainsString($this->emailAddress, Artisan::output()); + $this->assertEquals(0, $exitCode); + + Mail::assertSent(TestMail::class, function (TestMail $mail) { + $mail->build(); + $this->assertCount(1, $mail->to); + $this->assertEquals($this->emailAddress, $mail->to[0]['address']); + $this->assertCount(0, $mail->cc); + $this->assertCount(0, $mail->bcc); + + return true; + }); + } + + + /** @test */ + public function it_is_queued_with_the_queue_flag_only() + { + $exitCode = Artisan::call($this->command,[ + 'recipient' => $this->emailAddress, + '--queue' => null + ]); + + $this->assertStringContainsString($this->emailAddress, Artisan::output()); + $this->assertEquals(0, $exitCode); + + Mail::assertQueued(TestMail::class, function (TestMail $mail) { + $mail->build(); + $this->assertCount(1, $mail->to); + $this->assertEquals($this->emailAddress, $mail->to[0]['address']); + $this->assertCount(0, $mail->cc); + $this->assertCount(0, $mail->bcc); + + return true; + }); + } + + /** @test */ + public function it_is_queued_with_stack_only() + { + $exitCode = Artisan::call($this->command,[ + '--stack' => 'emails' + ]); + + $this->assertStringContainsString($this->emailAddress, Artisan::output()); + $this->assertEquals(0, $exitCode); + + Mail::assertQueued(TestMail::class, function (TestMail $mail) { + $mail->build(); + $this->assertEquals('emails', $mail->queue); + $this->assertCount(1, $mail->to); + $this->assertEquals($this->emailAddress, $mail->to[0]['address']); + $this->assertCount(0, $mail->cc); + $this->assertCount(0, $mail->bcc); + + return true; + }); + } + + /** @test */ + public function it_is_queued_with_connection_only() + { + $exitCode = Artisan::call($this->command,[ + '--connection' => 'sync' + ]); + + $this->assertStringContainsString($this->emailAddress, Artisan::output()); + $this->assertEquals(0, $exitCode); + + Mail::assertQueued(TestMail::class, function (TestMail $mail) { + $mail->build(); + $this->assertEquals('sync', $mail->connection); + $this->assertCount(1, $mail->to); + $this->assertEquals($this->emailAddress, $mail->to[0]['address']); + $this->assertCount(0, $mail->cc); + $this->assertCount(0, $mail->bcc); + + return true; + }); + } + + /** @test */ + public function it_is_queued_with_connection_and_stack() + { + $exitCode = Artisan::call($this->command,[ + '--stack' => 'emails', + '--connection' => 'sync' + ]); + + $this->assertStringContainsString($this->emailAddress, Artisan::output()); + $this->assertEquals(0, $exitCode); + + Mail::assertQueued(TestMail::class, function (TestMail $mail) { + $mail->build(); + $this->assertEquals('emails', $mail->queue); + $this->assertEquals('sync', $mail->connection); + $this->assertCount(1, $mail->to); + $this->assertEquals($this->emailAddress, $mail->to[0]['address']); + $this->assertCount(0, $mail->cc); + $this->assertCount(0, $mail->bcc); + + return true; + }); + } + + /** @test */ + public function it_is_queued_with__queue_flag_connection_and_stack() + { + $exitCode = Artisan::call($this->command,[ + '--queue' => null, + '--stack' => 'emails', + '--connection' => 'sync' + ]); + + $this->assertStringContainsString($this->emailAddress, Artisan::output()); + $this->assertEquals(0, $exitCode); + + Mail::assertQueued(TestMail::class, function (TestMail $mail) { + $mail->build(); + $this->assertEquals('emails', $mail->queue); + $this->assertEquals('sync', $mail->connection); + $this->assertCount(1, $mail->to); + $this->assertEquals($this->emailAddress, $mail->to[0]['address']); + $this->assertCount(0, $mail->cc); + $this->assertCount(0, $mail->bcc); + + return true; + }); + } + + /** @test */ + public function it_will_throw_an_exception_when_passing_an_invalid_mail_address() + { + $exitCode = Artisan::call($this->command, [ + 'recipient' => 'notanemailaddress', + ]); + + Mail::assertNotSent(TestMail::class); + + $this->assertEquals(1, $exitCode); + } + + /** @test */ + public function it_will_throw_an_exception_if_it_cannot_find_an_email_address() + { + config(['mail.from.address' => null]); + //$this->expectException(Exception::class); + + $exitCode = Artisan::call($this->command, [ + 'recipient' => 'notanemailaddress', + ]); + + Mail::assertNotQueued(TestMail::class); + + $this->assertEquals(1, $exitCode); + } + + /** @test */ + public function it_can_be_sent_using_different_mail_drivers() + { + config(['mail.driver' => 'smtp']); + + $this->assertEquals('smtp', config('mail.driver')); + $exitCode = Artisan::call($this->command); + + $this->assertStringContainsString('smtp', Artisan::output()); + $this->assertEquals(0, $exitCode); + + $exitCode = Artisan::call($this->command, ['--driver' => 'log']); + $this->assertStringContainsString('log', Artisan::output()); + $this->assertEquals(0, $exitCode); + } + + /** @test */ + public function the_driver_option_is_optional_but_cannot_be_blank() + { + config(['mail.driver' => 'smtp']); + + $this->assertEquals('smtp', config('mail.driver')); + $exitCode = Artisan::call($this->command, ['--driver'=> '']); + + $this->assertStringContainsString('The driver field is required.', Artisan::output()); + + Mail::assertNotSent(TestMail::class); + + $this->assertEquals(1, $exitCode); + } + + /** @test */ + public function input_validation_shows_multiple_errors() + { + config(['mail.driver' => 'smtp']); + + $this->assertEquals('smtp', config('mail.driver')); + + $exitCode = Artisan::call($this->command, [ + 'recipient' => 'notanemailaddress', + '--driver'=> '', + ] + ); + + $output = Artisan::output(); + + $this->assertStringContainsString('The email must be a valid email address.', $output); + $this->assertStringContainsString('The driver field is required.', $output); + + Mail::assertNotSent(TestMail::class); + + $this->assertEquals(1, $exitCode); + } +}