diff --git a/.travis.yml b/.travis.yml index 7d1272f..ca6f715 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: php php: - - 5.6 - 7.0 - 7.1 @@ -9,4 +8,4 @@ sudo: false install: travis_retry composer install --no-interaction --prefer-source -script: vendor/bin/phpunit --verbose \ No newline at end of file +script: vendor/bin/phpunit --verbose diff --git a/README.md b/README.md index 489f87f..c608569 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ## Introduction -This is a package that stores and queues e-mails using a database table. Easily send e-mails using a cronjob or schedule e-mails that should be sent at a specific date and time. +This is a package that stores and queues e-mails using a database table. Easily send e-mails using a cronjob and schedule e-mails that should be sent at a specific date and time. ## Installation First, require the package using composer. @@ -24,16 +24,15 @@ If you're running Laravel 5.5 or later you may skip this step. Add the service p Buildcode\LaravelDatabaseEmails\LaravelDatabaseEmailsServiceProvider::class, ``` -Publish the configuration file. +Publish the configuration files. ```bash $ php artisan vendor:publish --provider=Buildcode\\LaravelDatabaseEmails\\LaravelDatabaseEmailsServiceProvider ``` -Create the e-mails database table migration. +Create the database table required for this package. ```bash -$ php artisan email:table $ php artisan migrate ``` @@ -57,7 +56,7 @@ protected function schedule(Schedule $schedule) ### Create An Email ```php -Buildcode\LaravelDatabaseEmails\Email::compose() +Email::compose() ->label('welcome-mail-1.0') ->recipient('john@doe.com') ->subject('This is a test') @@ -68,20 +67,53 @@ Buildcode\LaravelDatabaseEmails\Email::compose() ->send(); ``` +### Specify Recipients + +```php +$one = 'john@doe.com'; +$multiple = ['john@doe.com', 'jane@doe.com']; + +Email::compose()->recipient($one); +Email::compose()->recipient($multiple); + +Email::compose()->cc($one); +Email::compose()->cc($multiple); + +Email::compose()->bcc($one); +Email::compose()->bcc($multiple); +``` + +### Mailables + +You may also pass a mailable to the e-mail composer. + +```php +Email::compose() + ->mailable(new OrderShipped()) + ->send(); +``` + +### Attachments + +```php +Email::compose() + ->attach('/path/to/file'); +``` + +Or for in-memory attachments... + +```php +Email::compose() + ->attachData('
Your order has shipped!
', 'order.html'); +``` + ### Schedule An Email -You may schedule an e-mail by calling `schedule` instead of `send` at the end of the chain. You must provide a Carbon instance or a strtotime valid date. +You may schedule an e-mail by calling `later` instead of `send` at the end of the chain. You must provide a Carbon instance or a strtotime valid date. ```php -Buildcode\LaravelDatabaseEmails\Email::compose() - ->label('welcome-mail-1.0') - ->recipient('john@doe.com') - ->subject('This is a test') - ->view('emails.welcome') - ->variables([ - 'name' => 'John Doe', - ]) - ->schedule('+2 hours'); +Email::compose() + ->later('+2 hours'); ``` ### Manually Sending E-mails @@ -112,11 +144,6 @@ If you wish to encrypt your e-mails, please enable the `encrypt` option in the c ### Testing Address -If you wish to send e-mails to a test address but don't necessarily want to use a service like mailtrap, please take a look at the `testing` configuration. This is turned on by default. +If you wish to send e-mails to a test address but don't necessarily want to use a service like mailtrap, please take a look at the `testing` configuration. This is turned off by default. During the creation of an e-mail, the recipient will be replaced by the test e-mail. This is useful for local development or testing on a staging server. - -## Todo - -- Add support for attachments -- Add support for Mailables diff --git a/composer.json b/composer.json index a5517c6..4aa1ecc 100644 --- a/composer.json +++ b/composer.json @@ -8,11 +8,6 @@ "email": "info@marickvantuil.nl" } ], - "require": { - "illuminate/database": "^5.2", - "illuminate/console": "^5.2", - "illuminate/validation": "^5.2" - }, "autoload": { "psr-4": { "Buildcode\\LaravelDatabaseEmails\\": "src/" @@ -31,8 +26,13 @@ } }, "require-dev": { - "orchestra/testbench": "~3.4", - "phpunit/phpunit": "^5.7", - "orchestra/database": "~3.4" + "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" } } diff --git a/config/laravel-database-emails.php b/config/laravel-database-emails.php index add485d..3913f35 100644 --- a/config/laravel-database-emails.php +++ b/config/laravel-database-emails.php @@ -48,7 +48,9 @@ 'email' => 'test@email.com', 'enabled' => function () { - return app()->environment('local', 'staging'); + return false; + // ...or... + // return app()->environment('local', 'staging'); } ], diff --git a/database/migrations/emails.stub b/database/migrations/2017_12_14_151403_create_emails_table.php similarity index 86% rename from database/migrations/emails.stub rename to database/migrations/2017_12_14_151403_create_emails_table.php index 24f873c..dc49d1f 100644 --- a/database/migrations/emails.stub +++ b/database/migrations/2017_12_14_151403_create_emails_table.php @@ -4,7 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; -class Create{{tableClassName}}Table extends Migration +class CreateEmailsTable extends Migration { /** * Run the migrations. @@ -13,7 +13,11 @@ class Create{{tableClassName}}Table extends Migration */ public function up() { - Schema::create('{{table}}', function (Blueprint $table) { + if (Schema::hasTable('emails')) { + return; + } + + Schema::create('emails', function (Blueprint $table) { $table->increments('id'); $table->string('label')->nullable(); $table->binary('recipient'); @@ -43,6 +47,6 @@ public function up() */ public function down() { - Schema::dropIfExists('{{table}}'); + // } } 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 new file mode 100644 index 0000000..f515068 --- /dev/null +++ b/database/migrations/2017_12_14_151421_add_attachments_to_emails_table.php @@ -0,0 +1,34 @@ +binary('attachments')->after('body')->default(''); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/src/Config.php b/src/Config.php index ee39831..57b924b 100644 --- a/src/Config.php +++ b/src/Config.php @@ -9,7 +9,7 @@ class Config * * @return int */ - public static function maxRetryCount() + public static function maxAttemptCount() { return max(config('laravel-database-emails.retry.attempts', 1), 3); } @@ -59,4 +59,4 @@ public static function cronjobEmailLimit() { return config('laravel-database-emails.limit', 20); } -} \ No newline at end of file +} diff --git a/src/CreateEmailTableCommand.php b/src/CreateEmailTableCommand.php deleted file mode 100644 index a1c271b..0000000 --- a/src/CreateEmailTableCommand.php +++ /dev/null @@ -1,112 +0,0 @@ -files = $files; - $this->composer = $composer; - } - - /** - * Execute the console command. - * - * @return void - */ - public function handle() - { - $table = 'emails'; - - $this->replaceMigration( - $this->createBaseMigration($table), $table, Str::studly($table) - ); - - $this->info('Migration created successfully!'); - - $this->composer->dumpAutoloads(); - } - - /** - * Execute the console command (backwards compatibility for Laravel 5.4 and below). - * - * @return void - */ - public function fire() - { - $this->handle(); - } - - /** - * Create a base migration file for the table. - * - * @param string $table - * @return string - */ - protected function createBaseMigration($table = 'emails') - { - return $this->laravel['migration.creator']->create( - 'create_' . $table . '_table', $this->laravel->databasePath() . '/migrations' - ); - } - - /** - * Replace the generated migration with the job table stub. - * - * @param string $path - * @param string $table - * @param string $tableClassName - * @return void - */ - protected function replaceMigration($path, $table, $tableClassName) - { - $stub = str_replace( - ['{{table}}', '{{tableClassName}}'], - [$table, $tableClassName], - $this->files->get(__DIR__ . '/../database/migrations/emails.stub') - ); - - $this->files->put($path, $stub); - } -} diff --git a/src/Decorators/EmailDecorator.php b/src/Decorators/EmailDecorator.php deleted file mode 100644 index d588f80..0000000 --- a/src/Decorators/EmailDecorator.php +++ /dev/null @@ -1,8 +0,0 @@ -email = $decorator->getEmail(); - - $this->email->fill([ - 'recipient' => encrypt($this->email->getRecipient()), - 'subject' => encrypt($this->email->getSubject()), - 'variables' => encrypt(json_encode($this->email->getVariables())), - 'body' => encrypt(view($this->email->getView(), $this->email->getVariables())->render()), - ]); - - if ($this->email->hasCc()) { - $this->email->cc = encrypt($this->email->getCc()); - } - - if ($this->email->hasBcc()) { - $this->email->bcc = encrypt($this->email->getBcc()); - } - } - - public function getEmail() - { - return $this->email; - } - - -} \ No newline at end of file diff --git a/src/Decorators/PrepareEmail.php b/src/Decorators/PrepareEmail.php deleted file mode 100644 index 75688e8..0000000 --- a/src/Decorators/PrepareEmail.php +++ /dev/null @@ -1,64 +0,0 @@ -email = $email; - - if ($email->hasCc()) { - if (!is_array($email->cc)) { - $email->cc = [$email->cc]; - } - - if (Config::testing()) { - $email->cc = array_map(function () { - return Config::testEmailAddress(); - }, $email->cc); - } - - $email->cc = json_encode($email->cc); - } - - if ($email->hasBcc()) { - if (!is_array($email->bcc)) { - $email->bcc = [$email->bcc]; - } - - if (Config::testing()) { - $email->bcc = array_map(function () { - return Config::testEmailAddress(); - }, $email->bcc); - } - - $email->bcc = json_encode($email->bcc); - } - - - $email->body = view($email->getView(), $email->hasVariables() ? $email->getVariables() : [])->render(); - - $email->variables = json_encode($email->getVariables()); - - if ($email->isScheduled()) { - $email->scheduled_at = $email->getScheduledDateAsCarbon()->toDateTimeString(); - } - - if (Config::testing()) { - $email->recipient = Config::testEmailAddress(); - } - - $this->email->encrypted = Config::encryptEmails(); - } - - public function getEmail() - { - return $this->email; - } -} \ No newline at end of file diff --git a/src/Email.php b/src/Email.php index 8e086e0..38d6505 100644 --- a/src/Email.php +++ b/src/Email.php @@ -2,15 +2,34 @@ namespace Buildcode\LaravelDatabaseEmails; -use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Mail; use Carbon\Carbon; use Exception; +/** + * @property $id + * @property $label + * @property $recipient + * @property $cc + * @property $bcc + * @property $subject + * @property $view + * @property $variables + * @property $body + * @property $attachments + * @property $attempts + * @property $sending + * @property $failed + * @property $error + * @property $encrypted + * @property $scheduled_at + * @property $sent_at + * @property $delivered_at + */ class Email extends Model { + use HasEncryptedAttributes; + /** * The table in which the e-mails are stored. * @@ -62,7 +81,19 @@ public function getLabel() */ public function getRecipient() { - return $this->getEmailProperty('recipient'); + return $this->recipient; + } + + /** + * Get the e-mail recipient(s) as string. + * + * @return string + */ + public function getRecipientsAsString() + { + $glue = ', '; + + return implode($glue, (array)$this->recipient); } /** @@ -72,12 +103,6 @@ public function getRecipient() */ public function getCc() { - if ($this->exists) { - $cc = $this->getEmailProperty('cc'); - - return json_decode($cc, 1); - } - return $this->cc; } @@ -88,12 +113,6 @@ public function getCc() */ public function getBcc() { - if ($this->exists) { - $bcc = $this->getEmailProperty('bcc'); - - return json_decode($bcc, 1); - } - return $this->bcc; } @@ -104,7 +123,7 @@ public function getBcc() */ public function getSubject() { - return $this->getEmailProperty('subject'); + return $this->subject; } /** @@ -125,16 +144,6 @@ public function getView() */ public function getVariables() { - if ($this->exists) { - $var = $this->getEmailProperty('variables'); - - return json_decode($var, 1); - } - - if (is_string($this->variables)) { - return json_decode($this->variables, 1); - } - return $this->variables; } @@ -145,7 +154,17 @@ public function getVariables() */ public function getBody() { - return $this->getEmailProperty('body'); + return $this->body; + } + + /** + * Get the e-mail attachments. + * + * @return array + */ + public function getAttachments() + { + return $this->attachments; } /** @@ -158,7 +177,6 @@ public function getAttempts() return $this->attempts; } - /** * Get the scheduled date. * @@ -220,7 +238,7 @@ public function getError() */ public function hasCc() { - return !is_null($this->cc); + return strlen($this->getOriginal('cc')) > 0; } /** @@ -230,7 +248,7 @@ public function hasCc() */ public function hasBcc() { - return !is_null($this->bcc); + return strlen($this->getOriginal('bcc')) > 0; } /** @@ -243,7 +261,6 @@ public function isScheduled() return !is_null($this->getScheduledDate()); } - /** * Determine if the e-mail is encrypted. * @@ -251,7 +268,7 @@ public function isScheduled() */ public function isEncrypted() { - return !!$this->encrypted; + return !!$this->getOriginal('encrypted'); } /** @@ -274,25 +291,6 @@ public function hasFailed() return $this->failed == 1; } - /** - * Get a decrypted property. - * - * @param string $property - * @return mixed - */ - private function getEmailProperty($property) - { - if ($this->exists && $this->isEncrypted()) { - try { - return decrypt($this->{$property}); - } catch (DecryptException $e) { - return ''; - } - } - - return $this->{$property}; - } - /** * Mark the e-mail as sending. * @@ -318,6 +316,8 @@ public function markAsSent() $this->update([ 'sending' => 0, 'sent_at' => $now, + 'failed' => 0, + 'error' => '', ]); } @@ -332,7 +332,7 @@ public function markAsFailed(Exception $exception) $this->update([ 'sending' => 0, 'failed' => 1, - 'error' => $exception->getMessage(), + 'error' => (string)$exception, ]); } @@ -343,26 +343,7 @@ public function markAsFailed(Exception $exception) */ public function send() { - if ($this->isSent()) { - return; - } - - $this->markAsSending(); - - if (app()->runningUnitTests()) { - Event::dispatch('before.send'); - } - - Mail::send([], [], function ($message) { - $message->to($this->getRecipient()) - ->cc($this->hasCc() ? $this->getCc() : []) - ->bcc($this->hasBcc() ? $this->getBcc() : []) - ->subject($this->getSubject()) - ->from(config('mail.from.address'), config('mail.from.name')) - ->setBody($this->getBody(), 'text/html'); - }); - - $this->markAsSent(); + (new Sender)->send($this); } /** diff --git a/src/EmailComposer.php b/src/EmailComposer.php index b7faaa0..67135f4 100644 --- a/src/EmailComposer.php +++ b/src/EmailComposer.php @@ -2,20 +2,85 @@ namespace Buildcode\LaravelDatabaseEmails; -use Buildcode\LaravelDatabaseEmails\Decorators\EncryptEmail; -use Buildcode\LaravelDatabaseEmails\Decorators\PrepareEmail; -use Carbon\Carbon; -use Psr\Log\InvalidArgumentException; +use Illuminate\Mail\Mailable; class EmailComposer { + /** + * The e-mail that is being composed. + * + * @var Email + */ private $email; + /** + * The e-email data. + * + * @var array + */ + protected $data = []; + + /** + * Create a new EmailComposer instance. + * + * @param Email $email + */ public function __construct(Email $email) { $this->email = $email; } + /** + * Get the e-mail that is being composed. + * + * @return Email + */ + public function getEmail() + { + return $this->email; + } + + /** + * Set a data value. + * + * @param string $key + * @param mixed $value + * @return static + */ + public function setData($key, $value) + { + $this->data[$key] = $value; + + return $this; + } + + /** + * Get a data value. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getData($key, $default = null) + { + if (!is_null($default) && !$this->hasData($key)) { + return $default; + } + + return $this->data[$key]; + } + + /** + * Determine if the given data value was set. + * + * @param string $key + * @return bool + */ + public function hasData($key) + { + return isset($this->data[$key]); + } + /** * Set the e-mail label. * @@ -24,48 +89,40 @@ public function __construct(Email $email) */ public function label($label) { - $this->email->label = $label; - - return $this; + return $this->setData('label', $label); } /** - * Set the e-mail recipient. + * Set the e-mail recipient(s). * - * @param string $recipient + * @param string|array $recipient * @return static */ public function recipient($recipient) { - $this->email->recipient = $recipient; - - return $this; + return $this->setData('recipient', $recipient); } /** - * Send a copy of this e-mail to the given addresses. + * Define the carbon-copy address(es). * - * @param array $cc + * @param string|array $cc * @return static */ public function cc($cc) { - $this->email->cc = $cc; - - return $this; + return $this->setData('cc', $cc); } /** - * Send a blind copy to the given addresses. + * Define the blind carbon-copy address(es). * - * @param array $bcc + * @param string|array $bcc * @return static */ public function bcc($bcc) { - $this->email->bcc = $bcc; - - return $this; + return $this->setData('bcc', $bcc); } /** @@ -76,9 +133,7 @@ public function bcc($bcc) */ public function subject($subject) { - $this->email->subject = $subject; - - return $this; + return $this->setData('subject', $subject); } /** @@ -89,37 +144,92 @@ public function subject($subject) */ public function view($view) { - $this->email->view = $view; - - return $this; + return $this->setData('view', $view); } /** * Set the e-mail variables. * * @param array $variables - * @return static + * @return EmailComposer */ public function variables($variables) { - $this->email->variables = $variables; + return $this->setData('variables', $variables); + } - return $this; + /** + * Schedule the e-mail. + * + * @param mixed $scheduledAt + * @return Email + */ + public function schedule($scheduledAt) + { + return $this->later($scheduledAt); } /** * Schedule the e-mail. * - * @param mixed $scheduledFor + * @param mixed $scheduledAt * @return Email */ - public function schedule($scheduledFor) + public function later($scheduledAt) { - $this->email->scheduled_at = $scheduledFor; + $this->setData('scheduled_at', $scheduledAt); return $this->send(); } + /** + * Set the Mailable. + * + * @param Mailable $mailable + * @return static + */ + public function mailable(Mailable $mailable) + { + $this->setData('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 = []) + { + $attachments = $this->hasData('attachments') ? $this->getData('attachments') : []; + + $attachments[] = compact('file', 'options'); + + return $this->setData('attachments', $attachments); + } + + /** + * Attach in-memory data as an attachment. + * + * @param string $data + * @param string $name + * @param array $options + * @return $this + */ + public function attachData($data, $name, array $options = []) + { + $attachments = $this->hasData('rawAttachments') ? $this->getData('rawAttachments') : []; + + $attachments[] = compact('data', 'name', 'options'); + + return $this->setData('rawAttachments', $attachments); + } + /** * Send the e-mail. * @@ -127,16 +237,16 @@ public function schedule($scheduledFor) */ public function send() { - Validator::validate($this->email); + (new Validator)->validate($this); + + (new Preparer)->prepare($this); if (Config::encryptEmails()) { - $email = (new EncryptEmail(new PrepareEmail($this->email)))->getEmail(); - } else { - $email = (new PrepareEmail($this->email))->getEmail(); + (new Encrypter)->encrypt($this); } - $email->save(); + $this->email->save(); - return $email; + return $this->email->fresh(); } -} \ No newline at end of file +} diff --git a/src/Encrypter.php b/src/Encrypter.php new file mode 100644 index 0000000..4a6649f --- /dev/null +++ b/src/Encrypter.php @@ -0,0 +1,96 @@ +setEncrypted($composer); + + $this->encryptRecipients($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 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 new file mode 100644 index 0000000..162e293 --- /dev/null +++ b/src/HasEncryptedAttributes.php @@ -0,0 +1,64 @@ +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 2080354..0b036d2 100644 --- a/src/LaravelDatabaseEmailsServiceProvider.php +++ b/src/LaravelDatabaseEmailsServiceProvider.php @@ -13,11 +13,15 @@ class LaravelDatabaseEmailsServiceProvider extends ServiceProvider */ public function boot() { - $dir = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR; + $baseDir = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR; + $configDir = $baseDir . 'config' . DIRECTORY_SEPARATOR; + $migrationsDir = $baseDir . 'database' . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR; $this->publishes([ - $dir . 'laravel-database-emails.php' => config_path('laravel-database-emails.php') + $configDir . 'laravel-database-emails.php' => config_path('laravel-database-emails.php'), ]); + + $this->loadMigrationsFrom([$migrationsDir]); } /** @@ -28,7 +32,6 @@ public function boot() public function register() { $this->commands([ - CreateEmailTableCommand::class, SendEmailsCommand::class, RetryFailedEmailsCommand::class, ]); diff --git a/src/MailableReader.php b/src/MailableReader.php new file mode 100644 index 0000000..44b8330 --- /dev/null +++ b/src/MailableReader.php @@ -0,0 +1,129 @@ +readRecipient($composer); + + $this->readCc($composer); + + $this->readBcc($composer); + + $this->readSubject($composer); + + $this->readBody($composer); + + $this->readAttachments($composer); + } + + /** + * Convert the mailable addresses array into a array with only e-mails. + * + * @param string $from + * @return array + */ + private function convertMailableAddresses($from) + { + return collect($from)->map(function ($recipient) { + return $recipient['address']; + })->toArray(); + } + + /** + * Read the mailable recipient to the email composer. + * + * @param EmailComposer $composer + */ + private function readRecipient(EmailComposer $composer) + { + $to = $this->convertMailableAddresses( + $composer->getData('mailable')->to + ); + + $composer->recipient($to); + } + + /** + * Read the mailable cc to the email composer. + * + * @param EmailComposer $composer + */ + private function readCc(EmailComposer $composer) + { + $cc = $this->convertMailableAddresses( + $composer->getData('mailable')->cc + ); + + $composer->cc($cc); + } + + /** + * Read the mailable bcc to the email composer. + * + * @param EmailComposer $composer + */ + private function readBcc(EmailComposer $composer) + { + $bcc = $this->convertMailableAddresses( + $composer->getData('mailable')->bcc + ); + + $composer->bcc($bcc); + } + + /** + * Read the mailable subject to the email composer. + * + * @param EmailComposer $composer + */ + private function readSubject(EmailComposer $composer) + { + $composer->subject($composer->getData('mailable')->subject); + } + + /** + * Read the mailable body to the email composer. + * + * @param EmailComposer $composer + * @throws Exception + */ + private function readBody(EmailComposer $composer) + { + if (app()->version() < '5.5') { + throw new Exception('Mailables cannot be read by Laravel 5.4 and below. Sorry.'); + } + + $composer->setData('view', ''); + + $composer->setData('body', $composer->getData('mailable')->render()); + } + + /** + * Read the mailable attachments to the email composer. + * + * @param EmailComposer $composer + */ + private function readAttachments(EmailComposer $composer) + { + $mailable = $composer->getData('mailable'); + + foreach ((array)$mailable->attachments as $attachment) { + call_user_func_array([$composer, 'attach'], $attachment); + } + + foreach ((array)$mailable->rawAttachments as $rawAttachment) { + call_user_func_array([$composer, 'attachData'], $rawAttachment); + } + } +} diff --git a/src/Preparer.php b/src/Preparer.php new file mode 100644 index 0000000..87cf576 --- /dev/null +++ b/src/Preparer.php @@ -0,0 +1,210 @@ +prepareLabel($composer); + + $this->prepareRecipient($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 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/SendEmailsCommand.php b/src/SendEmailsCommand.php index ab8437c..737cb3d 100644 --- a/src/SendEmailsCommand.php +++ b/src/SendEmailsCommand.php @@ -94,10 +94,10 @@ protected function result($emails) $this->line("\n"); - $this->table($headers, $emails->map(function ($email) { + $this->table($headers, $emails->map(function (Email $email) { return [ $email->getId(), - $email->getRecipient(), + $email->getRecipientsAsString(), $email->getSubject(), $email->hasFailed() ? 'Failed' : 'OK', ]; diff --git a/src/Sender.php b/src/Sender.php new file mode 100644 index 0000000..88bd8a8 --- /dev/null +++ b/src/Sender.php @@ -0,0 +1,64 @@ +isSent()) { + return; + } + + $email->markAsSending(); + + $this->getMailerInstance()->send([], [], function (Message $message) use ($email) { + $this->buildMessage($message, $email); + }); + + $email->markAsSent(); + } + + /** + * Get the instance of the Laravel mailer. + * + * @return Mailer + */ + private function getMailerInstance() + { + return app('mailer'); + } + + /** + * Build the e-mail message. + * + * @param Message $message + * @param Email $email + */ + private function buildMessage(Message $message, Email $email) + { + $message->to($email->getRecipient()) + ->cc($email->hasCc() ? $email->getCc() : []) + ->bcc($email->hasBcc() ? $email->getBcc() : []) + ->subject($email->getSubject()) + ->from(config('mail.from.address'), config('mail.from.name')) + ->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']); + } + } +} diff --git a/src/Store.php b/src/Store.php index b980a5e..9fc73c0 100644 --- a/src/Store.php +++ b/src/Store.php @@ -10,13 +10,10 @@ class Store /** * Get all queued e-mails. * - * @return Collection + * @return Collection|Email[] */ public function getQueue() { - $maxAttempts = Config::maxRetryCount(); - $emailLimit = Config::cronjobEmailLimit(); - $query = new Email; return $query @@ -26,11 +23,10 @@ public function getQueue() $query->whereNull('scheduled_at') ->orWhere('scheduled_at', '<=', Carbon::now()->toDateTimeString()); }) - ->where('failed', '=', 0) ->where('sending', '=', 0) - ->where('attempts', '<', $maxAttempts) + ->where('attempts', '<', Config::maxAttemptCount()) ->orderBy('created_at', 'asc') - ->limit($emailLimit) + ->limit(Config::cronjobEmailLimit()) ->get(); } @@ -38,7 +34,7 @@ public function getQueue() * Get all e-mails that failed to be sent. * * @param int $id - * @return Collection + * @return Collection|Email[] */ public function getFailed($id = null) { @@ -49,8 +45,9 @@ public function getFailed($id = null) $query->where('id', '=', $id); }) ->where('failed', '=', 1) + ->where('attempts', '>=', Config::maxAttemptCount()) ->whereNull('sent_at') ->whereNull('deleted_at') ->get(); } -} \ No newline at end of file +} diff --git a/src/Validator.php b/src/Validator.php index f428285..4f3bca7 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -8,109 +8,155 @@ class Validator { - private static $email; + /** + * The e-mail composer. + * + * @var EmailComposer + */ + protected $composer; /** - * Validate the given e-mail. + * Validate the data that was given to the e-mail composer. * - * @param Email $email + * @param EmailComposer $composer * @throws InvalidArgumentException */ - public static function validate(Email $email) + public function validate(EmailComposer $composer) { - self::$email = $email; - - self::validateRecipient(); - self::validateCcAndBcc(); - self::validateSubject(); - self::validateView(); - self::validateVariables(); - self::validateScheduled(); + $this->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 recipient. + * Validate the defined label. * + * @param EmailComposer $composer * @throws InvalidArgumentException */ - private static function validateRecipient() + private function validateLabel(EmailComposer $composer) { - if (strlen(self::$email->getRecipient()) == 0) { + 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'); } - if (!filter_var(self::$email->getRecipient(), FILTER_VALIDATE_EMAIL)) { - throw new InvalidArgumentException('No valid e-mail 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 CC and BCC e-mail addresses. + * Validate the carbon copy e-mail addresses. * + * @param EmailComposer $composer * @throws InvalidArgumentException */ - private static function validateCcAndBcc() + private function validateCc(EmailComposer $composer) { - $cc = self::$email->getCc(); - $bcc = self::$email->getBcc(); + if (!$composer->hasData('cc')) { + return; + } - foreach ([$cc, $bcc] as $source) { - if (is_null($source)) { - continue; + foreach ((array)$composer->getData('cc') as $cc) { + if (!filter_var($cc, FILTER_VALIDATE_EMAIL)) { + throw new InvalidArgumentException('E-mail address [' . $cc . '] is invalid'); } + } + } - if (!is_array($source)) { - $source = [$source]; - } + /** + * Validate the blind carbon copy e-mail addresses. + * + * @param EmailComposer $composer + * @throws InvalidArgumentException + */ + private function validateBcc(EmailComposer $composer) + { + if (!$composer->hasData('bcc')) { + return; + } - foreach ($source as $email) { - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - throw new InvalidArgumentException('Not a valid e-mail: [' . $email . ']'); - } + 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 subject. + * Validate the e-mail subject. * + * @param EmailComposer $composer * @throws InvalidArgumentException */ - private static function validateSubject() + private function validateSubject(EmailComposer $composer) { - if (strlen(self::$email->getSubject()) == 0) { + if (!$composer->hasData('subject')) { throw new InvalidArgumentException('No subject specified'); } } /** - * Validate the view. + * Validate the e-mail view. * - * @throws InvalidArgumentException + * @param EmailComposer $composer + * @throws InvalidARgumentException */ - private static function validateView() + private function validateView(EmailComposer $composer) { - if (strlen(self::$email->getView()) == 0) { + if ($composer->hasData('mailable')) { + return; + } + + if (!$composer->hasData('view')) { throw new InvalidArgumentException('No view specified'); } - if (!view(self::$email->getView())) { - throw new InvalidArgumentException('View [' . self::$email->getView() . '] does not exist'); + $view = $composer->getData('view'); + + if (!view()->exists($view)) { + throw new InvalidArgumentException('View [' . $view . '] does not exist'); } } /** - * Validate the variables. + * Validate the e-mail variables. * + * @param EmailComposer $composer * @throws InvalidArgumentException */ - private static function validateVariables() + private function validateVariables(EmailComposer $composer) { - if (!self::$email->hasVariables()) { - return; - } - - if (!is_array(self::$email->getVariables())) { + if ($composer->hasData('variables') && !is_array($composer->getData('variables'))) { throw new InvalidArgumentException('Variables must be an array'); } } @@ -118,26 +164,27 @@ private static function validateVariables() /** * Validate the scheduled date. * + * @param EmailComposer $composer * @throws InvalidArgumentException */ - private static function validateScheduled() + private function validateScheduled(EmailComposer $composer) { - $date = self::$email->getScheduledDate(); - - if (is_null($date)) { + if (!$composer->hasData('scheduled_at')) { return; } - if (!$date instanceof Carbon && !is_string($date)) { + $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($date)) { + if (is_string($scheduled)) { try { - Carbon::parse($date); + Carbon::parse($scheduled); } catch (Exception $e) { throw new InvalidArgumentException('Scheduled date could not be parsed by Carbon: ' . $e->getMessage()); } } } -} \ No newline at end of file +} diff --git a/tests/DatabaseInteractionTest.php b/tests/DatabaseInteractionTest.php index c7cc98e..368f6e5 100644 --- a/tests/DatabaseInteractionTest.php +++ b/tests/DatabaseInteractionTest.php @@ -2,8 +2,8 @@ namespace Tests; -use Buildcode\LaravelDatabaseEmails\Email; use Illuminate\Support\Facades\DB; +use Dompdf\Dompdf; class DatabaseInteractionTest extends TestCase { @@ -21,7 +21,6 @@ function recipient_should_be_saved_correctly() { $email = $this->sendEmail(['recipient' => 'john@doe.com']); - $this->assertEquals('john@doe.com', DB::table('emails')->find(1)->recipient); $this->assertEquals('john@doe.com', $email->getRecipient()); } @@ -161,4 +160,48 @@ function recipient_should_be_swapped_for_test_address_when_in_testing_mode() $this->assertEquals('test@address.com', $email->getRecipient()); } -} \ No newline at end of file + + /** @test */ + 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') + ->send(); + + $this->assertCount(2, $email->getAttachments()); + + $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']); + } + + /** @test */ + function in_memory_attachments_should_be_saved_correctly() + { + $pdf = new Dompdf; + $pdf->loadHtml('Hello CI!'); + $pdf->setPaper('A4', 'landscape'); + + $email = $this->composeEmail() + ->attachData($pdf->outputHtml(), 'generated.pdf', [ + 'mime' => 'application/pdf' + ]) + ->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 index 686b76b..9d16e0f 100644 --- a/tests/EncryptionTest.php +++ b/tests/EncryptionTest.php @@ -26,7 +26,8 @@ function the_recipient_should_be_encrypted_and_decrypted() { $email = $this->sendEmail(['recipient' => 'john@doe.com']); - $this->assertNotEquals('john@doe.com', $email->recipient); + $this->assertEquals('john@doe.com', decrypt($email->getOriginal('recipient'))); + $this->assertEquals('john@doe.com', $email->getRecipient()); } @@ -38,12 +39,11 @@ function cc_and_bb_should_be_encrypted_and_decrypted() 'bcc' => $bcc = ['jane+1@doe.com', 'jane+2@doe.com'] ]); - $email = $email->fresh(); + $this->assertEquals($cc, decrypt($email->getOriginal('cc'))); + $this->assertEquals($bcc, decrypt($email->getOriginal('bcc'))); - $this->assertNotEquals(json_encode($cc), $email->cc); $this->assertEquals($cc, $email->getCc()); - $this->assertNotEquals(json_encode($bcc), $email->bcc); - $this->assertEquals($bcc, $email->fresh()->getBcc()); + $this->assertEquals($bcc, $email->getBcc()); } /** @test */ @@ -51,7 +51,8 @@ function the_subject_should_be_encrypted_and_decrypted() { $email = $this->sendEmail(['subject' => 'test subject']); - $this->assertNotEquals('test subject', $email->subject); + $this->assertEquals('test subject', decrypt($email->getOriginal('subject'))); + $this->assertEquals('test subject', $email->getSubject()); } @@ -60,8 +61,15 @@ function the_variables_should_be_encrypted_and_decrypted() { $email = $this->sendEmail(['variables' => ['name' => 'Jane Doe']]); - $this->assertNotEquals(['name' => 'Jane Doe'], $email->variables); - $this->assertEquals(['name' => 'Jane Doe'], $email->getVariables()); + $this->assertEquals( + ['name'=> 'Jane Doe'], + decrypt($email->getOriginal('variables')) + ); + + $this->assertEquals( + ['name' => 'Jane Doe'], + $email->getVariables() + ); } /** @test */ @@ -71,7 +79,8 @@ function the_body_should_be_encrypted_and_decrypted() $expectedBody = "Name: Jane Doe\n"; - $this->assertNotEquals($expectedBody, $email->body); + $this->assertEquals($expectedBody, decrypt($email->getOriginal('body'))); + $this->assertEquals($expectedBody, $email->getBody()); } -} \ No newline at end of file +} diff --git a/tests/MailableReaderTest.php b/tests/MailableReaderTest.php new file mode 100644 index 0000000..c15ca5a --- /dev/null +++ b/tests/MailableReaderTest.php @@ -0,0 +1,104 @@ +mailable(new TestMailable()); + + $this->assertEquals(['john@doe.com'], $composer->getData('recipient')); + + $composer = Email::compose() + ->mailable( + (new TestMailable())->to(['jane@doe.com']) + ); + + $this->assertEquals(['john@doe.com', 'jane@doe.com'], $composer->getData('recipient')); + } + + /** @test */ + function it_extracts_cc_addresses() + { + $composer = Email::compose()->mailable(new TestMailable()); + + $this->assertEquals(['john+cc@doe.com', 'john+cc2@doe.com'], $composer->getData('cc')); + } + + /** @test */ + function it_extracts_bcc_addresses() + { + $composer = Email::compose()->mailable(new TestMailable()); + + $this->assertEquals(['john+bcc@doe.com', 'john+bcc2@doe.com'], $composer->getData('bcc')); + } + + /** @test */ + function it_extracts_the_subject() + { + $composer = Email::compose()->mailable(new TestMailable()); + + $this->assertEquals('Your order has shipped!', $composer->getData('subject')); + } + + /** @test */ + function it_extracts_the_body() + { + $composer = Email::compose()->mailable(new TestMailable()); + + $this->assertEquals("Name: John Doe\n", $composer->getData('body')); + } + + /** @test */ + function it_extracts_attachments() + { + $email = Email::compose()->mailable(new TestMailable())->send(); + + $attachments = $email->getAttachments(); + + $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']); + } +} + +class TestMailable extends Mailable +{ + /** + * Create a new message instance. + * + * @return void + */ + public function __construct() + { + $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'); + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this->view('tests::dummy', ['name' => 'John Doe']); + } +} diff --git a/tests/RetryFailedEmailsCommandTest.php b/tests/RetryFailedEmailsCommandTest.php index 9e483d7..5be3fdc 100644 --- a/tests/RetryFailedEmailsCommandTest.php +++ b/tests/RetryFailedEmailsCommandTest.php @@ -2,50 +2,51 @@ namespace Tests; -use Buildcode\LaravelDatabaseEmails\Store; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Event; class RetryFailedEmailsCommandTest extends TestCase { + function setUp() + { + parent::setUp(); + + $this->app['config']['laravel-database-emails.attempts'] = 3; + } + /** @test */ - function it_should_retry_sending_failed_emails() + function an_email_cannot_be_reset_if_the_max_attempt_count_has_not_been_reached() { - Event::listen('before.send', function () { - throw new \Exception('Simulating some random error'); - }); + $this->app['config']['mail.driver'] = 'does-not-exist'; - $email = $this->sendEmail(); + $this->sendEmail(); $this->artisan('email:send'); - $email = $email->fresh(); + $this->assertEquals(1, DB::table('emails')->count()); + + $this->artisan('email:retry'); - $this->assertTrue($email->fresh()->hasFailed()); $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:retry'); $this->assertEquals(2, DB::table('emails')->count()); - $this->assertEquals(1, (new Store())->getQueue()->count()); } /** @test */ function a_single_email_can_be_resent() { - Event::listen('before.send', function () { - throw new \Exception('Simulating some random error'); - }); - - $this->sendEmail(); - $this->sendEmail(); - - $this->artisan('email:send'); + $emailA = $this->sendEmail(); + $emailB = $this->sendEmail(); - $this->assertEquals(2, DB::table('emails')->count()); + // simulate emailB being failed... + $emailB->update(['failed' => 1, 'attempts' => 3]); - $this->artisan('email:retry', ['id' => 1]); + $this->artisan('email:retry', ['id' => 2]); $this->assertEquals(3, DB::table('emails')->count()); } -} \ No newline at end of file +} diff --git a/tests/SendEmailsCommandTest.php b/tests/SendEmailsCommandTest.php index 152080e..9fd7a24 100644 --- a/tests/SendEmailsCommandTest.php +++ b/tests/SendEmailsCommandTest.php @@ -2,9 +2,9 @@ namespace Tests; +use Buildcode\LaravelDatabaseEmails\Store; use Carbon\Carbon; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Event; class SendEmailsCommandTest extends TestCase { @@ -45,50 +45,17 @@ function an_email_should_not_be_sent_once_it_is_marked_as_sent() $this->assertEquals($firstSend, $email->fresh()->getSendDate()); } - /** @test */ - function an_email_should_be_locked_so_overlapping_cronjobs_cannot_send_an_already_processing_email() - { - $email = $this->sendEmail(); - - Event::listen('before.send', function () { - $this->artisan('email:send'); - }); - - $this->artisan('email:send'); - - $this->assertEquals(1, $email->fresh()->getAttempts()); - } - /** @test */ function if_an_email_fails_to_be_sent_it_should_be_logged_in_the_database() { - $email = $this->sendEmail(); - - Event::listen('before.send', function () { - throw new \Exception('Simulating some random error'); - }); + $this->app['config']['mail.driver'] = 'does-not-exist'; - $this->artisan('email:send'); - - $this->assertTrue($email->fresh()->hasFailed()); - $this->assertEquals('Simulating some random error', $email->fresh()->getError()); - } - - /** @test */ - function a_failed_email_should_not_be_sent_again() - { $email = $this->sendEmail(); - Event::listen('before.send', function () { - throw new \Exception('Simulating some random error'); - }); - - $this->artisan('email:send'); - - # 1 min later... $this->artisan('email:send'); - $this->assertEquals(1, $email->fresh()->getAttempts()); + $this->assertTrue($email->fresh()->hasFailed()); + $this->assertContains('Driver [does-not-exist] not supported.', $email->fresh()->getError()); } /** @test */ @@ -121,4 +88,39 @@ function an_email_should_never_be_sent_before_its_scheduled_date() $this->assertEquals(1, $email->getAttempts()); $this->assertNotNull($email->getSendDate()); } -} \ No newline at end of file + + /** @test */ + function emails_will_be_sent_until_max_try_count_has_been_reached() + { + $this->app['config']['mail.driver'] = 'does-not-exist'; + + $this->sendEmail(); + $this->assertCount(1, (new Store)->getQueue()); + $this->artisan('email:send'); + $this->assertCount(1, (new Store)->getQueue()); + $this->artisan('email:send'); + $this->assertCount(1, (new Store)->getQueue()); + $this->artisan('email:send'); + $this->assertCount(0, (new Store)->getQueue()); + } + + /** @test */ + 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', + 'attempts' => 1, + ]); + + $this->assertTrue($email->fresh()->hasFailed()); + $this->assertEquals('Simulating some random error', $email->fresh()->getError()); + + $this->artisan('email:send'); + + $this->assertFalse($email->fresh()->hasFailed()); + $this->assertEmpty($email->fresh()->getError()); + } +} diff --git a/tests/SenderTest.php b/tests/SenderTest.php new file mode 100644 index 0000000..e7d95da --- /dev/null +++ b/tests/SenderTest.php @@ -0,0 +1,167 @@ +registerPlugin(new TestingMailEventListener($this)); + } + + /** @test */ + function it_sends_an_email() + { + $this->sendEmail(); + + Mail::shouldReceive('send') + ->once(); + + $this->artisan('email:send'); + } + + /** @test */ + function the_email_has_a_correct_from_email_and_from_name() + { + $this->app['config']->set('mail.from.address', 'testfromaddress@gmail.com'); + $this->app['config']->set('mail.from.name', 'From CI test'); + + $this->sendEmail(); + + $this->artisan('email:send'); + + $from = reset($this->sent)->getMessage()->getFrom(); + + $this->assertEquals('testfromaddress@gmail.com', key($from)); + $this->assertEquals('From CI test', $from[key($from)]); + } + + /** @test */ + function it_sends_emails_to_the_correct_recipients() + { + $this->sendEmail(['recipient' => 'john@doe.com']); + $this->artisan('email:send'); + $to = reset($this->sent)->getMessage()->getTo(); + $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(); + $this->assertCount(2, $to); + $this->assertArrayHasKey('john@doe.com', $to); + $this->assertArrayHasKey('john+2@doe.com', $to); + } + + /** @test */ + function it_adds_the_cc_addresses() + { + $this->sendEmail(['cc' => 'cc@test.com']); + $this->artisan('email:send'); + $cc = reset($this->sent)->getMessage()->getCc(); + $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(); + $this->assertCount(2, $cc); + $this->assertArrayHasKey('cc@test.com', $cc); + $this->assertArrayHasKey('cc+2@test.com', $cc); + } + + /** @test */ + function it_adds_the_bcc_addresses() + { + $this->sendEmail(['bcc' => 'bcc@test.com']); + $this->artisan('email:send'); + $bcc = reset($this->sent)->getMessage()->getBcc(); + $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(); + $this->assertCount(2, $bcc); + $this->assertArrayHasKey('bcc@test.com', $bcc); + $this->assertArrayHasKey('bcc+2@test.com', $bcc); + } + + /** @test */ + function the_email_has_the_correct_subject() + { + $this->sendEmail(['subject' => 'Hello World']); + + $this->artisan('email:send'); + + $subject = reset($this->sent)->getMessage()->getSubject(); + + $this->assertEquals('Hello World', $subject); + } + + /** @test */ + 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); + + $this->sent = []; + $this->sendEmail(['variables' => []]); + $this->artisan('email:send'); + $body = reset($this->sent)->getMessage()->getBody(); + $this->assertEquals(view('tests::dummy'), $body); + } + + /** @test */ + function attachments_are_added_to_the_email() + { + $this->composeEmail() + ->attach(__DIR__ . '/files/pdf-sample.pdf') + ->send(); + $this->artisan('email:send'); + + $attachments = reset($this->sent)->getMessage()->getChildren(); + $attachment = reset($attachments); + + $this->assertCount(1, $attachments); + $this->assertEquals('attachment; filename=pdf-sample.pdf',$attachment->getHeaders()->get('content-disposition')->getFieldBody()); + $this->assertEquals('application/pdf', $attachment->getContentType()); + } + + /** @test */ + function raw_attachments_are_added_to_the_email() + { + $pdf = new Dompdf; + $pdf->loadHtml('Hello CI!'); + $pdf->setPaper('A4'); + + $this->composeEmail() + ->attachData($pdf->outputHtml(), 'hello-ci.pdf', [ + 'mime' => 'application/pdf' + ]) + ->send(); + $this->artisan('email:send'); + + $attachments = reset($this->sent)->getMessage()->getChildren(); + $attachment = reset($attachments); + + $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()); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 4b108f6..026376d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,7 +3,6 @@ namespace Tests; use Buildcode\LaravelDatabaseEmails\Email; -use Illuminate\Database\Schema\Blueprint; use Eloquent; class Testcase extends \Orchestra\Testbench\TestCase @@ -26,41 +25,11 @@ function () { }, ]; - $this->createSchema(); + $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); view()->addNamespace('tests', __DIR__ . '/views'); } - /** - * Setup the database schema. - * - * @return void - */ - public function createSchema() - { - $this->schema()->create('emails', function (Blueprint $table) { - $table->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->integer('attempts')->default(0); - $table->boolean('sending')->default(0); - $table->boolean('failed')->default(0); - $table->text('error')->nullable(); - $table->boolean('encrypted')->default(0); - $table->timestamp('scheduled_at')->nullable(); - $table->timestamp('sent_at')->nullable(); - $table->timestamp('delivered_at')->nullable(); - $table->timestamps(); - $table->softDeletes(); - }); - } - /** * Get a database connection instance. * @@ -132,6 +101,11 @@ public function createEmail($overwrite = []) ->variables($params['variables']); } + public function composeEmail($overwrite = []) + { + return $this->createEmail($overwrite); + } + public function sendEmail($overwrite = []) { return $this->createEmail($overwrite)->send(); @@ -141,4 +115,4 @@ public function scheduleEmail($scheduledFor, $overwrite = []) { return $this->createEmail($overwrite)->schedule($scheduledFor); } -} \ No newline at end of file +} diff --git a/tests/TestingMailEventListener.php b/tests/TestingMailEventListener.php new file mode 100644 index 0000000..b143350 --- /dev/null +++ b/tests/TestingMailEventListener.php @@ -0,0 +1,21 @@ +test = $test; + } + + public function beforeSendPerformed($event) + { + $this->test->sent[] = $event; + } +} diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 8ac7278..2b96d3c 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -8,6 +8,17 @@ class PackageTest extends TestCase { + /** + * @test + * @expectedException InvalidArgumentException + */ + function a_label_cannot_contain_more_than_255_characters() + { + Email::compose() + ->label(str_repeat('a', 256)) + ->send(); + } + /** * @test * @expectedException InvalidArgumentException @@ -22,7 +33,7 @@ function a_recipient_is_required() /** * @test * @expectedException InvalidArgumentException - * @expectedExceptionMessage No valid e-mail specified + * @expectedExceptionMessage E-mail address [not-a-valid-email-address] is invalid */ function the_recipient_email_must_be_valid() { @@ -34,7 +45,7 @@ function the_recipient_email_must_be_valid() /** * @test * @expectedException InvalidArgumentException - * @expectedExceptionMessage Not a valid e-mail: [not-a-valid-email-address] + * @expectedExceptionMessage E-mail address [not-a-valid-email-address] is invalid */ function cc_must_contain_valid_email_addresses() { @@ -50,7 +61,7 @@ function cc_must_contain_valid_email_addresses() /** * @test * @expectedException InvalidArgumentException - * @expectedExceptionMessage Not a valid e-mail: [not-a-valid-email-address] + * @expectedExceptionMessage E-mail address [not-a-valid-email-address] is invalid */ function bcc_must_contain_valid_email_addresses() { @@ -173,4 +184,4 @@ function the_scheduled_date_must_be_a_carbon_instance_or_a_valid_date() } } -} \ No newline at end of file +} diff --git a/tests/files/pdf-sample.pdf b/tests/files/pdf-sample.pdf new file mode 100644 index 0000000..f698ff5 Binary files /dev/null and b/tests/files/pdf-sample.pdf differ