diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 2f2bee4..2680a49 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,49 +1,64 @@ name: run-tests -on: [push, pull_request] +on: + - push + - pull_request + - workflow_dispatch jobs: - test: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - php: [8.0, 8.1, 8.2] - laravel: [9.*, 8.*] - stability: [prefer-stable] - exclude: - - php: 8.2 - laravel: 8.* - include: - - laravel: 9.* - testbench: 7.* - - laravel: 8.* - testbench: 6.23 - - name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick - coverage: none - -# - name: Setup problem matchers -# run: | -# echo "::add-matcher::${{ runner.tool_cache }}/php.json" -# echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" -# - - name: Install dependencies - run: | - composer install - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update - composer update --${{ matrix.stability }} --prefer-dist --no-interaction - - - name: Execute tests - run: vendor/bin/phpunit + test: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + php: [8.0, 8.1, 8.2] + laravel: ['8.*', '9.*', '10.*', '11.*', '12.*'] + stability: [prefer-stable] + exclude: + - php: 8.0 + laravel: 10.* + - php: 8.2 + laravel: 8.* + - laravel: 11.* + php: 8.0 + - laravel: 11.* + php: 8.1 + - laravel: 12.* + php: 8.0 + - laravel: 12.* + php: 8.1 + include: + - laravel: 10.* + testbench: 8.* + - laravel: 9.* + testbench: 7.* + - laravel: 8.* + testbench: 6.23 + - laravel: 11.* + testbench: 9.* + - laravel: 12.* + testbench: 10.* + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + coverage: none + + - name: Install dependencies + run: | + composer install + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --dev --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction + + - name: Execute tests + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index 6cc69e4..cd85aa3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor coverage .phpunit.result.cache .idea +.phpunit.cache \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index df16b68..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,19 +0,0 @@ -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/.travis.yml b/.travis.yml deleted file mode 100644 index 2780ef2..0000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: php - -php: - - 7.3 - - 7.4 - - 8.0 - -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/README.md b/README.md index ffe1a31..e50c4c4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Laravel Mailbox 📬 [![Latest Version on Packagist](https://img.shields.io/packagist/v/beyondcode/laravel-mailbox.svg?style=flat-square)](https://packagist.org/packages/beyondcode/laravel-mailbox) -[![Build Status](https://img.shields.io/travis/beyondcode/laravel-mailbox/master.svg?style=flat-square)](https://travis-ci.org/beyondcode/laravel-mailbox) -[![Quality Score](https://img.shields.io/scrutinizer/g/beyondcode/laravel-mailbox.svg?style=flat-square)](https://scrutinizer-ci.com/g/beyondcode/laravel-mailbox) [![Total Downloads](https://img.shields.io/packagist/dt/beyondcode/laravel-mailbox.svg?style=flat-square)](https://packagist.org/packages/beyondcode/laravel-mailbox) Handle incoming emails in your Laravel application. @@ -11,15 +9,11 @@ Handle incoming emails in your Laravel application. Mailbox::from('{username}@gmail.com', function (InboundEmail $email, $username) { // Access email attributes and content $subject = $email->subject(); - + $email->reply(new ReplyMailable); }); ``` -[![https://phppackagedevelopment.com](https://beyondco.de/courses/phppd.jpg)](https://phppackagedevelopment.com) - -If you want to learn how to create reusable PHP packages yourself, take a look at my upcoming [PHP Package Development](https://phppackagedevelopment.com) video course. - ## Installation @@ -33,6 +27,16 @@ composer require beyondcode/laravel-mailbox Take a look at the [official documentation](https://docs.beyondco.de/laravel-mailbox). +## Catch, test and debug application mails with Laravel Herd + +Laravel Herd provides an integrated local email service, streamlining the process of testing and debugging application emails. +The email service organizes emails into distinct inboxes for each application, ensuring they are easily accessible and simple to locate. + +[herd.laravel.com](https://herd.laravel.com) + +![image](https://github.com/user-attachments/assets/6417907c-119d-43ac-9cf6-5638bafae24f) + + ### Testing ``` bash diff --git a/composer.json b/composer.json index 54e00ba..44a1f50 100644 --- a/composer.json +++ b/composer.json @@ -17,19 +17,19 @@ ], "require": { "php": "^8.0", - "illuminate/container": "^6.0|^7.0|^8.0|^9.0", - "illuminate/database": "^6.0|^7.0|^8.0|^9.0", - "illuminate/log": "^6.0|^7.0|^8.0|^9.0", - "illuminate/routing": "^6.0|^7.0|^8.0|^9.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0", + "illuminate/container": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/database": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/log": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/routing": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "willdurand/email-reply-parser": "^2.8", - "zbateson/mail-mime-parser": "^1.1" + "zbateson/mail-mime-parser": "^1.1|^2.4|^3.0" }, "require-dev": { "laminas/laminas-mail": "^2.13", "mockery/mockery": "^1.2", - "orchestra/testbench": "^4.0|^5.0|^7.0", - "phpunit/phpunit": "^7.0|^8.0|^9.3" + "orchestra/testbench": "^4.0|^5.0|^7.0|^8.0|^9.0|^10.0", + "phpunit/phpunit": "^7.0|^8.0|^9.3|^10.5|^11.5.3" }, "autoload": { "psr-4": { diff --git a/docs/drivers/drivers.md b/docs/drivers/drivers.md index d3896e0..1136e5f 100644 --- a/docs/drivers/drivers.md +++ b/docs/drivers/drivers.md @@ -53,12 +53,16 @@ Be sure the check the box labeled "Post the raw, full MIME message." when settin ## MailCare +::: warning +To use MailCare with Laravel Mailbox, you will need to generate a random password and store it as the `MAILBOX_HTTP_PASSWORD` environment variable. The default username is "laravel-mailbox", but you can change it using the `MAILBOX_HTTP_USERNAME` environment variable. +::: + You can then set your `MAILBOX_DRIVER` to "mailcare". -Next you will need to configure MailCare, to send incoming emails to your application at `/laravel-mailbox/mailcare`: -- Activate authentication and automation features. -- Create a new automation with the URL `https://your-application.com/laravel-mailbox/mailcare` -- Be sure the check the box labeled "Post the raw, full MIME message." +Next you will need to configure MailCare, to send incoming emails to your application at `/laravel-mailbox/mailcare`. +- Ask support to activate authentication and automation features. +- Create a new automation, if your application is at `https://awesome-laravel.com`, it would be with the URL `https://MAILBOX_HTTP_USERNAME:MAILBOX_HTTP_PASSWORD@awesome-laravel.com/laravel-mailbox/mailcare` +- Be sure the check the box labeled "Post the raw, full MIME message " See ["MailCare"](https://mailcare.io) for more information. @@ -66,5 +70,5 @@ See ["MailCare"](https://mailcare.io) for more information. When working locally, you might not want to use real incoming emails while testing your application. Out of the box, this package supports Laravel's "log" mail driver for incoming emails. -To test incoming emails, set both your `MAIL_DRIVER` and your `MAILBOX_DRIVER` in your `.env` file to "log". +To test incoming emails, set both your `MAIL_MAILER` and your `MAILBOX_DRIVER` in your `.env` file to "log". Now every time you send an email in your application, this email will appear in your `laravel.log` file and will try to call one of your configured Mailboxes. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 89d7e61..8a7588d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,33 +1,12 @@ - - - - tests - - - - - src/ - - - - - - - - - - - - - + + + + tests + + + + + + diff --git a/phpunit.xml.dist.bak b/phpunit.xml.dist.bak new file mode 100644 index 0000000..6967f7f --- /dev/null +++ b/phpunit.xml.dist.bak @@ -0,0 +1,21 @@ + + + + + tests + + + + + + + diff --git a/src/Console/CleanEmails.php b/src/Console/CleanEmails.php index 2631192..d682313 100644 --- a/src/Console/CleanEmails.php +++ b/src/Console/CleanEmails.php @@ -12,6 +12,8 @@ class CleanEmails extends Command protected $description = 'Clean up old incoming email logs.'; + protected $amountDeleted = 0; + public function handle() { $this->comment('Cleaning old incoming email logs...'); @@ -29,13 +31,18 @@ public function handle() /** @var InboundEmail $modelClass */ $modelClass = config('mailbox.model'); - $models = $modelClass::where('created_at', '<', $cutOffDate)->get(); + // chunk the deletion to avoid memory issues - $models->each->delete(); + $this->amountDeleted = 0; - $amountDeleted = $models->count(); + $modelClass::where('created_at', '<', $cutOffDate) + ->select('id') + ->eachById(count: 100, callback: function ($model) { + $model->delete(); + $this->amountDeleted++; + }); - $this->info("Deleted {$amountDeleted} record(s) from the Mailbox logs."); + $this->info("Deleted {$this->amountDeleted} record(s) from the Mailbox logs."); $this->comment('All done!'); } diff --git a/src/Http/Middleware/MailboxBasicAuthentication.php b/src/Http/Middleware/MailboxBasicAuthentication.php index 09020fc..6e747d9 100644 --- a/src/Http/Middleware/MailboxBasicAuthentication.php +++ b/src/Http/Middleware/MailboxBasicAuthentication.php @@ -12,7 +12,7 @@ public function handle($request, Closure $next) $user = $request->getUser(); $password = $request->getPassword(); - if (($user === config('mailbox.basic_auth.username') && $password === config('mailbox.basic_auth.password'))) { + if ($user === config('mailbox.basic_auth.username') && $password === config('mailbox.basic_auth.password')) { return $next($request); } diff --git a/src/Http/Requests/MailCareRequest.php b/src/Http/Requests/MailCareRequest.php index fd4405b..0e2215d 100644 --- a/src/Http/Requests/MailCareRequest.php +++ b/src/Http/Requests/MailCareRequest.php @@ -4,19 +4,28 @@ use BeyondCode\Mailbox\InboundEmail; use Illuminate\Foundation\Http\FormRequest; -use Illuminate\Support\Facades\Validator; class MailCareRequest extends FormRequest { - public function validator() + public function rules() { - return Validator::make($this->all(), [ - 'email' => 'required', + return [ + 'content_type' => 'required|in:message/rfc2822', + ]; + } + + public function prepareForValidation() + { + $this->merge([ + 'content_type' => $this->headers->get('Content-type'), ]); } public function email() { - return InboundEmail::fromMessage($this->get('email')); + /** @var InboundEmail $modelClass */ + $modelClass = config('mailbox.model'); + + return $modelClass::fromMessage($this->getContent()); } } diff --git a/src/InboundEmail.php b/src/InboundEmail.php index 089bb02..6e24fff 100644 --- a/src/InboundEmail.php +++ b/src/InboundEmail.php @@ -141,7 +141,7 @@ public function attachments() public function message(): MimeMessage { - $this->mimeMessage = $this->mimeMessage ?: MimeMessage::from($this->message); + $this->mimeMessage = $this->mimeMessage ?: MimeMessage::from($this->message, true); return $this->mimeMessage; } @@ -170,7 +170,8 @@ public function forward($recipients) return Mail::send([], [], function ($message) use ($recipients) { $message->to($recipients) ->subject($this->subject()) - ->setBody($this->body(), $this->message()->getContentType()); + ->text($this->text()) + ->html($this->html()); }); } @@ -193,4 +194,43 @@ public function isValid(): bool { return $this->from() !== '' && ($this->isText() || $this->isHtml()); } + + public function isAutoReply($checkCommonSubjects = true): bool + { + if ($this->headerValue('x-autorespond')) { + return true; + } + + if (in_array($this->headerValue('precedence'), ['auto_reply', 'bulk', 'junk'])) { + return true; + } + + if (in_array($this->headerValue('x-precedence'), ['auto_reply', 'bulk', 'junk'])) { + return true; + } + if (in_array($this->headerValue('auto-submitted'), ['auto-replied', 'auto-generated'])) { + return true; + } + + if ($checkCommonSubjects) { + return Str::startsWith($this->subject(), [ + 'Auto:', + 'Automatic reply', + 'Autosvar', + 'Automatisk svar', + 'Automatisch antwoord', + 'Abwesenheitsnotiz', + 'Risposta Non al computer', + 'Automatisch antwoord', + 'Auto Response', + 'Respuesta automática', + 'Fuori sede', + 'Out of Office', + 'Frånvaro', + 'Réponse automatique', + ]); + } + + return false; + } } diff --git a/src/Routing/Route.php b/src/Routing/Route.php index 33e4eca..e4f9ff2 100644 --- a/src/Routing/Route.php +++ b/src/Routing/Route.php @@ -83,24 +83,24 @@ protected function gatherMatchSubjectsFromMessage(InboundEmail $message) switch ($this->subject) { case self::FROM: return [$message->from()]; - break; + break; case self::TO: return $this->convertMessageAddresses($message->to()); - break; + break; case self::CC: return $this->convertMessageAddresses($message->cc()); - break; + break; case self::BCC: return $this->convertMessageAddresses($message->bcc()); break; case self::SUBJECT: return [$message->subject()]; - break; + break; } } /** - * @param $addresses AddressPart[] + * @param $addresses AddressPart[] * @return array */ protected function convertMessageAddresses($addresses): array diff --git a/src/Routing/Router.php b/src/Routing/Router.php index 390d839..4b32946 100644 --- a/src/Routing/Router.php +++ b/src/Routing/Router.php @@ -24,7 +24,7 @@ class Router /** @var Container */ protected $container; - public function __construct(Container $container = null) + public function __construct(?Container $container) { $this->container = $container ?: new Container; diff --git a/tests/Console/CleanEmailsTest.php b/tests/Console/CleanEmailsTest.php index 78f88cd..5f18272 100644 --- a/tests/Console/CleanEmailsTest.php +++ b/tests/Console/CleanEmailsTest.php @@ -23,14 +23,14 @@ public function setUp(): void /** @test */ public function it_can_clean_the_statistics() { - Collection::times(60)->each(function (int $index) { + Collection::times(200)->each(function (int $index) { InboundEmail::forceCreate([ 'message' => Str::random(), 'created_at' => Carbon::now()->subDays($index)->startOfDay(), ]); }); - $this->assertCount(60, InboundEmail::all()); + $this->assertCount(200, InboundEmail::all()); Artisan::call('mailbox:clean'); @@ -46,19 +46,19 @@ public function it_errors_if_max_age_inf() { $this->app['config']->set('mailbox.store_incoming_emails_for_days', INF); - Collection::times(60)->each(function (int $index) { + Collection::times(200)->each(function (int $index) { InboundEmail::forceCreate([ 'message' => Str::random(), 'created_at' => Carbon::now()->subDays($index)->startOfDay(), ]); }); - $this->assertCount(60, InboundEmail::all()); + $this->assertCount(200, InboundEmail::all()); $this->artisan('mailbox:clean') ->expectsOutput('mailbox:clean is disabled because store_incoming_emails_for_days is set to INF.') ->assertExitCode(1); - $this->assertCount(60, InboundEmail::all()); + $this->assertCount(200, InboundEmail::all()); } } diff --git a/tests/MailboxRouteTest.php b/tests/MailboxRouteTest.php index 7e1de29..5adfcd0 100644 --- a/tests/MailboxRouteTest.php +++ b/tests/MailboxRouteTest.php @@ -8,7 +8,7 @@ class MailboxRouteTest extends TestCase { - public function emailDataProvider() + public static function emailDataProvider() { return [ ['hello@beyondco.de', 'hello@beyondco.de', 'wrong@beyondco.de'], @@ -18,6 +18,7 @@ public function emailDataProvider() /** * @test + * * @dataProvider emailDataProvider */ public function it_matches_from_mails($fromMail, $successfulPattern, $failingPattern) @@ -36,6 +37,7 @@ public function it_matches_from_mails($fromMail, $successfulPattern, $failingPat /** * @test + * * @dataProvider emailDataProvider */ public function it_matches_to_mails($toMail, $successfulPattern, $failingPattern) @@ -54,6 +56,7 @@ public function it_matches_to_mails($toMail, $successfulPattern, $failingPattern /** * @test + * * @dataProvider emailDataProvider */ public function it_matches_cc_mails($ccMail, $successfulPattern, $failingPattern) @@ -72,6 +75,7 @@ public function it_matches_cc_mails($ccMail, $successfulPattern, $failingPattern /** * @test + * * @dataProvider emailDataProvider */ public function it_matches_bcc_mails($bccMail, $successfulPattern, $failingPattern) @@ -90,6 +94,7 @@ public function it_matches_bcc_mails($bccMail, $successfulPattern, $failingPatte /** * @test + * * @dataProvider subjectDataProvider */ public function it_matches_subjects($subject, $successfulPattern, $failingPattern) @@ -125,7 +130,7 @@ public function it_matches_requirements() $this->assertTrue($route->matches($message)); } - public function subjectDataProvider() + public static function subjectDataProvider() { return [ ['New Laravel Packages', 'New Laravel Packages', 'Old Laravel Packages'],