While working on a university information system we were encountering a phenomenon when some emails sent to users would fail to send. The university used Microsoft infrastructure for sending emails via SMTP, and well, for some esoteric reason one in 100 or so were failing to reach the recepient. Of course the failed emails needed to be resent, and Queue package seemed like the obvious choice to handle that.
Here is how we achieved the desired change basically without refactoring of the codebase.
First of all, we simply followed the Queue documentation to the letter in installing the package in our Codeigniter app and creating the Email queue files. You can read about that step here.
Now we had to decide where to tweak the code to send the failed emails to queue for sending retry later. There were a lot of code blocks in our code that looked like this:
$email = service('email'); $email->setFrom('noreply@lsu.lt', 'LSU informacinė sistema'); $email->setTo($user->email); $email->setSubject($messageSubject); $email->setMessage($messageBody); $success = $email->send(false);
So the obvious choice at first seemed to add a code block that would check if email was successfully sent and if not – send it’s data to the queue:
if (! $success) { $emailData = [ 'recipients' => $user->email, 'subject' => $messageSubject, 'body' => $messageBody, 'fromEmail' => 'noreply@lsu.lt', 'fromName' => 'LSU informacinė sistema', ]; service('queue')->push('emails', 'email', $emailData); }
It would work fine, but this code had to be added in many different places.
So we decided to extend the Codeigniter 4 Email library instead. We added file app/Libraries/Email.php:
<?php namespace App\Libraries; use CodeIgniter\Email\Email as BaseEmail; use ErrorException; class Email extends BaseEmail { /** * Store the raw subject. * * @var string */ protected $rawSubject = ''; /** * Raw debug messages * * @var list<string> */ private array $debugMessageRaw = []; /** * Indicates whether the email is being executed by the queue. * * @var bool */ protected $isQueueExecution = false; /** * Indicates whether the email is being moved to the queue instead of sending. * * @var bool */ protected $toQueue = false; /** * Spool mail to the mail server * * @return bool */ protected function spoolEmail() { $this->unwrapSpecials(); $protocol = $this->getProtocol(); $method = 'sendWith' . ucfirst($protocol); if($this->toQueue){ $this->addToQueue(); return true; } try { $success = $this->{$method}(); } catch (ErrorException $e) { $success = false; log_message('error', 'Email: ' . $method . ' throwed ' . $e); } if (! $success) { $message = lang('Email.sendFailure' . ($protocol === 'mail' ? 'PHPMail' : ucfirst($protocol))); log_message('error', 'Email: ' . $message); log_message('error', $this->printDebuggerRaw()); $this->setErrorMessage($message); // Avoid recursive queueing if (! $this->isQueueExecution) { $this->addToQueue(); } return false; } $this->setErrorMessage(lang('Email.sent', [$protocol])); return true; } /** * Returns raw debug messages * copy of the original method, which is set to private unfortunately */ private function printDebuggerRaw(): string { return implode("\n", $this->debugMessageRaw); } /** * Set the queue execution flag. */ public function setQueueExecution(bool $isQueue): void { $this->isQueueExecution = $isQueue; } /** * Set toQueue flag. */ public function setToQueue(bool $toQueue): void { $this->toQueue = $toQueue; } /** * Add the failed email to the queue. */ protected function addToQueue(): void { $queueData = [ 'recipients' => $this->recipients, 'cc' => $this->CCArray, 'bcc' => $this->BCCArray, // $this->subject results in empty string, had to make an owerride 'subject' => $this->getRawSubject(), 'body' => $this->body, 'headers' => $this->headers, 'fromEmail' => $this->fromEmail, 'fromName' => $this->fromName, // 'attachments' => $this->attachments, // needs more work ]; // Use queue library to add the data service('queue')->push('emails', 'email', $queueData); } /** * Override setSubject to store the raw subject. * * @param string $subject * * @return $this */ public function setSubject($subject) { $this->rawSubject = $subject; // Store the raw subject return parent::setSubject($subject); // Call the parent method } /** * Get the raw subject. */ public function getRawSubject(): string { return $this->rawSubject; } }
The main change here is this: the extended class contains an overridden method spoolEmail(), which is responsible for sending emails; here, when an email fails to send, it automatically executes a new method addToQueue(),which sends the data of email to the queue for retrying later. (Some other changes needed to be added as well, they are explained in the comments of the code).
After this, we also added this code:
public static function email($config = null, bool $getShared = true) { if ($getShared) { return static::getSharedInstance('email', $config); } if (empty($config) || (! is_array($config) && ! $config instanceof EmailConfig)) { $config = config(EmailConfig::class); } return new Email($config); }
to app/Config/Services.php file, so that the Codeigniter’s Email service would use our extended class in place of the default Codeigniter’s Email class.
And that was it. Now, without any other changes to the old code, once an email fails to send, it is transferred to the emails queue and retries are made to send it later.
As you can see, if email is sent from the queue and fails, it is NOT readded to the queue, the code block
if (! $this->isQueueExecution) { $this->addToQueue(); }
takes care of that.
And if we have a cycle where a lot of emails need to be sent and we do not want the user to wait for those emails to send, we can call the method setToQueue() with parameter ‘true’ before the $email->send():
$email->setToQueue(true); $email->send(false);
And, instead of sending the email immediately, it will be transferred to the queue to be dealt with later. And the user will be returned the page faster (as database operations are much faster than sending emails).
Here is how, with very little change in the code, we solved the problem of failed emails; and added a lot of scalability to our app.
Image credit goes to ChatGPT
Parašykite komentarą