Create event listener for Cake Mailer

I have a mailer in my app that’s currently called in a model towards the end of the controller’s save event:

// controller call
$this->Msrs->sendSignMail($recipients, $msr, $grandTotal, $user->full_name, $module);

// model function that calls the mailer
public function sendSignMail($recipients, $msr, $grandTotal, $fullName, $module)
{ 
    foreach ($recipients as $r) {
        $this->getMailer('Msr')->send('signMsr', [$msr, $grandTotal, $r->email, $r->first_name, $fullName, $module]);
    }
}

Here is the signMsr() method in MsrMailer:

public function signMsr($msr, $grandTotal, $email, $firstName, $requester, $module)
{ 
    $this
    ->setEmailFormat('html')
    ->setTo($email)
    ->setFrom([// 'from' email address])
    ->setSubject(sprintf($subjStart . $subject))
    ->setViewVars([
        // ViewVars here
    ])
    ->viewBuilder()
        ->setTemplate('sign_msr') 
        ->setLayout('default');
}

This all works fine as long as the recipient list is short (say, 1-3 people); however, if there are many recipients, the save process bogs down, and SMTP will sometimes time out before all of the emails are sent.

Because of our corporate email setup, I have to send the messages securely, over port 465, so I know that generates additional overhead. I feel like this could be solved using an afterSave event handler, but I’m stumped as to how to go about it. Here’s what I’ve got in my controller:

$mailer = new MsrMailer();
$this->Msrs->getEventManager()->on($this->getMailer('Msr'));

And here’s what I’ve got so far in MsrMailer:

public function implementedEvents() : array
{
    return [
        'Model.afterSave' => 'sendTheMail'
    ];
}

public function sendTheMail(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
   // What do I do here????
}

This is skeletal, obviously. I don’t know what the next step is in terms of fleshing out sendTheMail so that signMsr gets executed for every recipient and the mail gets sent. I’m not even sure it’s being called, because I can’t debug $event, $entity, or $options. Nothing happens after the save is complete. I can confirm, however, that implementedEvents() is firing, or at least being recognized.

What do I do next to get this working?

Anything you do in afterSave still runs in the same PHP process, so moving processing there doesn’t speed up the response that you send to the browser, or eliminate timeouts that happen due to SMTP conversations. The only way to eliminate all of that is to save the email details to your database and have an entirely separate process that runs regularly in the background to send out any queued emails.

Copy. So, I’m looking at a cronjob, then?

Yep, that’s the idea.

If email is working for a few but not for many it could be a forced restriction by either the ISP or your company/corporation policy. That is, rate limiting the number of emails which can be sent - so yup, cronjob and check for any rate limiting spam protection rules.

The rate limit isn’t the problem. The timeouts are intermittent based on server load. Sometimes the process just takes too long and it times out. Others, the messages go through fine, albeit slowly. I’m looking into divorcing the mail sending process completely from my controller via cron.

You can try triggering a command instead of a cronjob. If the process is shortlived it could work.

I have something like this:

$memoryLimit = env('EXPORT_MEMORY_LIMIT', '1G');
exec(
    'sh -c "php -d memory_limit=' . $memoryLimit . ' ' . ROOT .
    '/bin/cake.php ExecuteExport ' . $export->id .
    ' > /dev/null 2>&1 &"'
);

So the command does all the work, present the end user a message like “your export is being processed” and at the end send the mail with the result.

If you can separate the processes, so the web serving aspect is not burdened by a background task then that may be better. Another concern is your webpage provider may kill a task which runs for more than 1 minute (mine does) as its purpose is to serve pages, not do processing. So you may trigger that too! In which case, break it up in batches of x-many emails a minute or whatever.

1 Like