#123: The framework of sending e-mails using Mailables but queued in the database is now there. Password reset e-mails are now the first ones being sent using the queue.
This commit is contained in:
parent
bfbf740810
commit
7418438d63
141
app/Console/Commands/SendEmailsCommand.php
Normal file
141
app/Console/Commands/SendEmailsCommand.php
Normal file
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\EmailLog;
|
||||
use App\Facade\UserConfig;
|
||||
use App\Http\Middleware\GlobalConfiguration;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Mail\Mailer;
|
||||
use Illuminate\Mail\Message;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class SendEmailsCommand extends Command
|
||||
{
|
||||
const MAX_EMAILS_PER_BATCH = 100;
|
||||
|
||||
const MAX_NUMBER_ATTEMPTS = 5;
|
||||
|
||||
const SECONDS_TO_SLEEP = 30;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bt-queue:send-emails {--poll}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Sends e-mails queued in the database.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
GlobalConfiguration::updateMailConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (!UserConfig::get('queue_emails'))
|
||||
{
|
||||
$this->output->error('E-mail queueing is not enabled. E-mails are being sent immediately.');
|
||||
}
|
||||
|
||||
$this->output->writeln('E-mail queue runner started');
|
||||
|
||||
while (true)
|
||||
{
|
||||
$emailsToSend = EmailLog::where([
|
||||
['sent_at', null],
|
||||
['number_attempts', '<', self::MAX_NUMBER_ATTEMPTS]
|
||||
])->limit(self::MAX_EMAILS_PER_BATCH)->get();
|
||||
|
||||
$this->output->writeln(sprintf(
|
||||
'%d e-mail%s to send',
|
||||
$emailsToSend->count(),
|
||||
$emailsToSend->count() == 1 ? '' : 's'
|
||||
));
|
||||
|
||||
/** @var EmailLog $emailToSend */
|
||||
foreach ($emailsToSend as $emailToSend)
|
||||
{
|
||||
$this->sendEmail($emailToSend);
|
||||
}
|
||||
|
||||
if (!$this->option('poll'))
|
||||
{
|
||||
exit();
|
||||
}
|
||||
|
||||
sleep(self::SECONDS_TO_SLEEP);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendEmail(EmailLog $emailLog)
|
||||
{
|
||||
$this->output->writeln(sprintf('Sending message with subject \'%s\'', $emailLog->subject));
|
||||
|
||||
try
|
||||
{
|
||||
app('mailer')->send(
|
||||
[],
|
||||
[],
|
||||
function (Message $message) use ($emailLog)
|
||||
{
|
||||
$message->setFrom($emailLog->sender_address, $emailLog->sender_name);
|
||||
$message->setSubject($emailLog->subject);
|
||||
|
||||
$this->addAddresses($emailLog->to_addresses, $message, 'To');
|
||||
$this->addAddresses($emailLog->cc_addresses, $message, 'Cc');
|
||||
$this->addAddresses($emailLog->bcc_addresses, $message, 'Bcc');
|
||||
|
||||
$message->addPart($emailLog->body_plain, 'text/plain');
|
||||
$message->setBody($emailLog->body_html, 'text/html');
|
||||
}
|
||||
);
|
||||
|
||||
$emailLog->sent_at = new \DateTime();
|
||||
|
||||
$this->output->writeln('Send completed');
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
$this->output->error(sprintf('Send failed: %s', $ex->getMessage()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
$emailLog->number_attempts++;
|
||||
$emailLog->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function addAddresses($dbFieldData, Message $message, $property)
|
||||
{
|
||||
$decoded = json_decode($dbFieldData);
|
||||
|
||||
if (is_array($decoded))
|
||||
{
|
||||
foreach ($decoded as $addressInfo)
|
||||
{
|
||||
$this->output->writeln(sprintf('Adding %s address: \'"%s" <%s>\'', $property, $addressInfo->name, $addressInfo->address));
|
||||
|
||||
$message->{"set{$property}"}($addressInfo->address, $addressInfo->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
app/EmailLog.php
Normal file
26
app/EmailLog.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EmailLog extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'sender_user_id',
|
||||
'queued_at',
|
||||
'sent_at',
|
||||
'sender_name',
|
||||
'sender_address',
|
||||
'to_addresses',
|
||||
'cc_addresses',
|
||||
'subject',
|
||||
'body_plain',
|
||||
'body_html'
|
||||
];
|
||||
}
|
@ -23,6 +23,44 @@ class GlobalConfiguration
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
public static function updateMailConfig()
|
||||
{
|
||||
/** @var Mailer $mailer */
|
||||
$mailer = app('mailer');
|
||||
$swiftMailer = $mailer->getSwiftMailer();
|
||||
|
||||
/** @var \Swift_SmtpTransport $transport */
|
||||
$transport = $swiftMailer->getTransport();
|
||||
$transport->setHost(UserConfig::get('smtp_server'));
|
||||
$transport->setPort(intval(UserConfig::get('smtp_port')));
|
||||
|
||||
$username = UserConfig::get('smtp_username');
|
||||
if (!is_null($username))
|
||||
{
|
||||
$transport->setUsername($username);
|
||||
}
|
||||
|
||||
$password = UserConfig::get('smtp_password');
|
||||
if (!is_null($password))
|
||||
{
|
||||
try
|
||||
{
|
||||
$transport->setPassword(decrypt($password));
|
||||
}
|
||||
catch (DecryptException $ex)
|
||||
{
|
||||
// Unable to decrypt the password - presumably the app's key has changed
|
||||
}
|
||||
}
|
||||
|
||||
if (UserConfig::get('smtp_encryption'))
|
||||
{
|
||||
$transport->setEncryption('tls');
|
||||
}
|
||||
|
||||
$mailer->alwaysFrom(UserConfig::get('sender_address'), UserConfig::get('sender_name'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new middleware instance.
|
||||
*
|
||||
@ -170,42 +208,4 @@ class GlobalConfiguration
|
||||
View::share('app_version', $version);
|
||||
View::share('app_version_url', $version);
|
||||
}
|
||||
|
||||
private function updateMailConfig()
|
||||
{
|
||||
/** @var Mailer $mailer */
|
||||
$mailer = $this->app->mailer;
|
||||
$swiftMailer = $mailer->getSwiftMailer();
|
||||
|
||||
/** @var \Swift_SmtpTransport $transport */
|
||||
$transport = $swiftMailer->getTransport();
|
||||
$transport->setHost(UserConfig::get('smtp_server'));
|
||||
$transport->setPort(intval(UserConfig::get('smtp_port')));
|
||||
|
||||
$username = UserConfig::get('smtp_username');
|
||||
if (!is_null($username))
|
||||
{
|
||||
$transport->setUsername($username);
|
||||
}
|
||||
|
||||
$password = UserConfig::get('smtp_password');
|
||||
if (!is_null($password))
|
||||
{
|
||||
try
|
||||
{
|
||||
$transport->setPassword(decrypt($password));
|
||||
}
|
||||
catch (DecryptException $ex)
|
||||
{
|
||||
// Unable to decrypt the password - presumably the app's key has changed
|
||||
}
|
||||
}
|
||||
|
||||
if (UserConfig::get('smtp_encryption'))
|
||||
{
|
||||
$transport->setEncryption('tls');
|
||||
}
|
||||
|
||||
$mailer->alwaysFrom(UserConfig::get('sender_address'), UserConfig::get('sender_name'));
|
||||
}
|
||||
}
|
41
app/Mail/MailableBase.php
Normal file
41
app/Mail/MailableBase.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\EmailLog;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
abstract class MailableBase extends Mailable
|
||||
{
|
||||
public function buildEmailLog()
|
||||
{
|
||||
// Build the e-mail
|
||||
$this->build();
|
||||
|
||||
// Get the current user for the ID
|
||||
$currentUser = Auth::user();
|
||||
|
||||
// Build the body so we can use it as a string
|
||||
$bodies = $this->buildView();
|
||||
|
||||
/** @var HtmlString $html */
|
||||
$html = $bodies['html'];
|
||||
|
||||
/** @var HtmlString $text */
|
||||
$text = $bodies['text'];
|
||||
|
||||
return new EmailLog([
|
||||
'sender_user_id' => !is_null($currentUser) ? $currentUser->id : null,
|
||||
'sender_name' => $this->from[0]['name'],
|
||||
'sender_address' => $this->from[0]['address'],
|
||||
'to_addresses' => json_encode($this->to),
|
||||
'cc_addresses' => json_encode($this->cc),
|
||||
'bcc_addresses' => json_encode($this->bcc),
|
||||
'subject' => $this->subject,
|
||||
'body_plain' => $text->toHtml(),
|
||||
'body_html' => $html->toHtml()
|
||||
]);
|
||||
}
|
||||
}
|
48
app/Mail/ResetMyPassword.php
Normal file
48
app/Mail/ResetMyPassword.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Facade\Theme;
|
||||
use App\Facade\UserConfig;
|
||||
use App\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ResetMyPassword extends MailableBase
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
private $token;
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(User $user, $token)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$subject = trans('email.reset_my_password_subject', ['app_name' => UserConfig::get('app_name')]);
|
||||
|
||||
return $this
|
||||
->subject($subject)
|
||||
->markdown(Theme::viewName('email.reset_my_password'))
|
||||
->with([
|
||||
'subject' => $subject,
|
||||
'token' => $this->token,
|
||||
'user' => $this->user
|
||||
]);
|
||||
}
|
||||
}
|
22
app/Notifications/EmailDatabaseWriterChannelBase.php
Normal file
22
app/Notifications/EmailDatabaseWriterChannelBase.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\EmailLog;
|
||||
|
||||
class EmailDatabaseWriterChannelBase
|
||||
{
|
||||
protected function writeToTable(EmailLog $logEntry, $shouldQueue = false)
|
||||
{
|
||||
if ($shouldQueue)
|
||||
{
|
||||
$logEntry->queued_at = new \DateTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
$logEntry->sent_at = new \DateTime();
|
||||
}
|
||||
|
||||
$logEntry->save();
|
||||
}
|
||||
}
|
24
app/Notifications/QueueEmailDatabaseChannel.php
Normal file
24
app/Notifications/QueueEmailDatabaseChannel.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\EmailLog;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class QueueEmailDatabaseChannel extends EmailDatabaseWriterChannelBase
|
||||
{
|
||||
/**
|
||||
* Send the given notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @param \Illuminate\Notifications\Notification $notification
|
||||
* @return void
|
||||
*/
|
||||
public function send($notifiable, Notification $notification)
|
||||
{
|
||||
/** @var EmailLog $logEntry */
|
||||
$logEntry = $notification->toQueueEmailDatabase($notifiable);
|
||||
|
||||
$this->writeToTable($logEntry, true);
|
||||
}
|
||||
}
|
@ -2,7 +2,11 @@
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Facade\UserConfig;
|
||||
use App\Mail\MailableBase;
|
||||
use App\Mail\ResetMyPassword;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
@ -37,33 +41,40 @@ class ResetPassword extends Notification
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
$drivers = [];
|
||||
|
||||
if (UserConfig::get('queue_emails'))
|
||||
{
|
||||
$drivers[] = QueueEmailDatabaseChannel::class;
|
||||
}
|
||||
else
|
||||
{
|
||||
$drivers[] = 'mail';
|
||||
$drivers[] = SentEmailDatabaseChannel::class;
|
||||
}
|
||||
|
||||
return $drivers;
|
||||
}
|
||||
|
||||
public function toQueueEmailDatabase($notifiable)
|
||||
{
|
||||
return $this->toMail($notifiable)->buildEmailLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage|MailableBase
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
return (new MailMessage)
|
||||
->line('You are receiving this email because we received a password reset request for your account.')
|
||||
->action('Reset Password', route('password.reset', $this->token, true))
|
||||
->line('If you did not request a password reset, no further action is required.');
|
||||
}
|
||||
$notification = new ResetMyPassword($notifiable, $this->token);
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
// Set to and from properties accordingly
|
||||
$notification->from(UserConfig::get('sender_address'), UserConfig::get('sender_name'));
|
||||
$notification->to($notifiable->email, $notifiable->name);
|
||||
|
||||
return $notification;
|
||||
}
|
||||
}
|
||||
|
24
app/Notifications/SentEmailDatabaseChannel.php
Normal file
24
app/Notifications/SentEmailDatabaseChannel.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\EmailLog;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class SentEmailDatabaseChannel extends EmailDatabaseWriterChannelBase
|
||||
{
|
||||
/**
|
||||
* Send the given notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @param \Illuminate\Notifications\Notification $notification
|
||||
* @return void
|
||||
*/
|
||||
public function send($notifiable, Notification $notification)
|
||||
{
|
||||
/** @var EmailLog $logEntry */
|
||||
$logEntry = $notification->toDatabaseWriter($notifiable);
|
||||
|
||||
$this->writeToTable($logEntry, false);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=7.0.0",
|
||||
"ext-json": "*",
|
||||
"laravel/framework": "5.5.*",
|
||||
"rackspace/php-opencloud": "^1.16",
|
||||
"doctrine/dbal": "^2.5",
|
||||
|
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateJobsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('background_jobs', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('background_jobs');
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateEmailLogsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('email_logs', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedInteger('sender_user_id')->nullable(true);
|
||||
$table->dateTime('queued_at')->nullable(true);
|
||||
$table->dateTime('sent_at')->nullable(true);
|
||||
$table->string('sender_name');
|
||||
$table->string('sender_address');
|
||||
$table->text('to_addresses');
|
||||
$table->text('cc_addresses')->nullable(true);
|
||||
$table->text('bcc_addresses')->nullable(true);
|
||||
$table->string('subject');
|
||||
$table->longText('body_plain');
|
||||
$table->longText('body_html');
|
||||
$table->integer('number_attempts')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('sender_user_id')
|
||||
->references('id')->on('users')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('email_logs');
|
||||
}
|
||||
}
|
@ -30,6 +30,10 @@ return [
|
||||
'photo_comment_replied_to_p1' => 'A reply to your comment has been posted in the :album_name album.',
|
||||
'photo_comment_replied_to_p2' => 'Click the button below to view the photo\'s page to see the reply to your comment.',
|
||||
'photo_comment_replied_to_subject' => 'A reply to your comment was posted in :album_name',
|
||||
'reset_my_password_action' => 'Reset Password',
|
||||
'reset_my_password_p1' => 'You are receiving this email because we received a password reset request for your account.',
|
||||
'reset_my_password_p2' => 'If you did not request a password reset, no further action is required.',
|
||||
'reset_my_password_subject' => 'Your password reset link for :app_name',
|
||||
'user_self_activated_p1' => 'A new user has been created and activated at :app_name. The user\'s details are shown below.',
|
||||
'user_self_activated_p2' => 'Name: :user_name',
|
||||
'user_self_activated_p3' => 'E-mail address: :email_address',
|
||||
|
@ -0,0 +1,19 @@
|
||||
@component('mail::message')
|
||||
@lang('email.generic_intro', ['user_name' => $user->name])
|
||||
|
||||
|
||||
@lang('email.reset_my_password_p1')
|
||||
|
||||
|
||||
@component('mail::button', ['url' => route('password.reset', $token, true), 'color' => 'blue'])
|
||||
@lang('email.reset_my_password_action')
|
||||
@endcomponent
|
||||
|
||||
|
||||
@lang('email.reset_my_password_p2')
|
||||
|
||||
|
||||
@lang('email.generic_regards')<br/>
|
||||
{{ UserConfig::get('app_name') }}<br/>
|
||||
<a href="{{ route('home') }}">{{ route('home') }}</a>
|
||||
@endcomponent
|
Loading…
Reference in New Issue
Block a user