diff --git a/config/default.php b/config/default.php index 285036e..d30ffe0 100644 --- a/config/default.php +++ b/config/default.php @@ -61,4 +61,14 @@ return [ 'php' => [ 'timezone' => 'Europe/London', ], + + /* + * ---------------------------------------------------------------------- + * Retry Mailer settings + * ---------------------------------------------------------------------- + */ + 'retry_mailer' => [ + 'enabled' => true, + 'max_retries' => 5, + ], ]; diff --git a/src/Jobs/RetryEmail.php b/src/Jobs/RetryEmail.php new file mode 100644 index 0000000..b5cc2be --- /dev/null +++ b/src/Jobs/RetryEmail.php @@ -0,0 +1,133 @@ +message = $mailMessage; + } + } + + + /** + * {@inheritdoc} + */ + public function setParams(array $params) + { + $mailMessage = new StaticMailMessage(); + $mailMessage->setFromEmail($params['from_email']); + $mailMessage->setFromName($params['from_name']); + $mailMessage->setReplyEmail($params['reply_email']); + $mailMessage->setReplyName($params['reply_name']); + + foreach ($params['recipients'] as $recipient) { + $mailRecipient = new EmailRecipient( + $recipient['email'], + $recipient['name'], + $recipient['params'] + ); + foreach ($recipient['ccs'] as $cc) { + $mailRecipient->cc($cc['email'], $cc['name']); + } + foreach ($recipient['bccs'] as $bcc) { + $mailRecipient->bcc($bcc['email'], $bcc['name']); + } + $mailMessage->addEmailRecipient($mailRecipient); + } + + $mailMessage->setSubject($params['subject']); + $mailMessage->setBody($params['body']); + + $this->message = $mailMessage; + $this->retries = $params['retries']; + } + + /** + * {@inheritdoc} + */ + public function getParams() + { + $recipients = []; + foreach ($this->message->getRecipients() as $recipient) { + $recipients[] = [ + 'email' => $recipient->getEmail(), + 'name' => $recipient->getName(), + 'params' => $recipient->getParams(), + 'ccs' => $recipient->getCCs(), + 'bccs' => $recipient->getBCCs(), + ]; + } + + return [ + 'from_email' => $this->message->getFromEmail(), + 'from_name' => $this->message->getFromName(), + 'reply_email' => $this->message->getReplyEmail(), + 'reply_name' => $this->message->getReplyName(), + 'recipients' => $recipients, + 'subject' => $this->message->renderSubject(), + 'body' => $this->message->renderBody(), + 'retries' => $this->retries, + ]; + } + + /** + * Attempt to re-send the email up to a maximum number of times specified in the config + */ + public function run() { + try { + static::$ci->mailer->send($this->message, false, true); + } catch (\PHPMailer\PHPMailer\Exception $e) { + $this->retries = $this->retries + 1; + + if ($this->retries < static::$ci->config['retry_mailer.max_retries']) { + $this->queue(); + } else { + $allParams = $this->getParams(); + $allParams['body'] = null; + static::$ci->errorLogger->error( + "Maximum retries exceeded when attempting to send email:", + $allParams + ); + } + } + + return true; + } + + /** + * Terminate the current job + */ + public function terminate() { + // Nothing to do + } +} diff --git a/src/Mail/RetryMailer.php b/src/Mail/RetryMailer.php new file mode 100644 index 0000000..c512924 --- /dev/null +++ b/src/Mail/RetryMailer.php @@ -0,0 +1,122 @@ +setFromEmail($message->getFromEmail()); + $retryMailMessage->setFromName($message->getFromName()); + $retryMailMessage->setReplyEmail($message->getReplyEmail()); + $retryMailMessage->setReplyName($message->getReplyName()); + if ($recipient) { + $retryMailMessage->addEmailRecipient($recipient); + $retryMailMessage->setSubject($message->renderSubject($recipient->getParams())); + $retryMailMessage->setBody($message->renderBody($recipient->getParams())); + } else { + foreach ($message->getRecipients() as $recipientLoop) { + $retryMailMessage->addEmailRecipient($recipientLoop); + } + $retryMailMessage->setSubject($message->renderSubject()); + $retryMailMessage->setBody($message->renderBody()); + } + + $retry = new RetryEmail($retryMailMessage); + $retry->queue(); + } + + /** + * Send a MailMessage message. + * + * Sends a single email to all recipients, as well as their CCs and BCCs. + * Since it is a single-header message, recipient-specific template data will not be included. + * + * @param MailMessage $message + * @param bool $clearRecipients Set to true to clear the list of recipients in the message after calling send(). This helps avoid accidentally sending a message multiple times. + * + * @throws phpmailerException The message could not be sent. + */ + public function send(MailMessage $message, $clearRecipients = true, $disableRetry = false) + { + try { + return parent::send($message, $clearRecipients); + } catch(\PHPMailer\PHPMailer\Exception $e) { + if (!$disableRetry) { + $this->queueRetry($message); + } else { + throw $e; + } + } finally { + // Clear recipients from the PHPMailer object for this iteration, + // so that we can send a separate email to the next recipient. + $this->phpMailer->clearAllRecipients(); + } + + // Clear out the MailMessage's internal recipient list if requested + if ($clearRecipients) { + $message->clearRecipients(); + } + } + + /** + * Send a MailMessage message, sending a separate email to each recipient. + * + * If the message object supports message templates, this will render the template with the corresponding placeholder values for each recipient. + * + * @param MailMessage $message + * @param bool $clearRecipients Set to true to clear the list of recipients in the message after calling send(). This helps avoid accidentally sending a message multiple times. + * + * @throws phpmailerException The message could not be sent. + */ + public function sendDistinct(MailMessage $message, $clearRecipients = true, $disableRetry = false) + { + $allRecipients = $message->getRecipients(); + $message->clearRecipients(); + + foreach ($allRecipients as $recipient) { + $message->addEmailRecipient($recipient); + try { + return parent::sendDistinct($message, $clearRecipients); + } catch (\PHPMailer\PHPMailer\Exception $e) { + if (!$disableRetry) { + $this->queueRetry($message, $recipient); + } else { + throw $e; + } + } finally { + // Clear recipients from the PHPMailer object for this iteration, + // so that we can send a separate email to the next recipient. + $this->phpMailer->clearAllRecipients(); + } + $message->clearRecipients(); + } + + if (!$clearRecipients) { + foreach ($allRecipients as $recipient) { + $message->addEmailRecipient($recipient); + } + } + } +} diff --git a/src/ServicesProvider/ServicesProvider.php b/src/ServicesProvider/ServicesProvider.php index 9962827..d551c4a 100644 --- a/src/ServicesProvider/ServicesProvider.php +++ b/src/ServicesProvider/ServicesProvider.php @@ -19,6 +19,7 @@ use Psr\Http\Message\ServerRequestInterface as Request; use UserFrosting\Sprinkle\Core\Log\MixedFormatter; use UserFrosting\Sprinkle\UFTweaks\Twig\HasRoleExtension; use UserFrosting\Sprinkle\UFTweaks\Twig\MobileDetectExtension; +use UserFrosting\Sprinkle\UFTweaks\Mail\RetryMailer; /** @@ -209,5 +210,26 @@ class ServicesProvider return $logger; }; + + /* + * Mail service. + * + * @return \UserFrosting\Sprinkle\Core\Mail\Mailer + * @return \UserFrosting\Sprinkle\UFTweaks\Mail\RetryMailer + */ + $container->extend('mailer', function ($mailer, $c) { + if (!$c->config['retry_mailer.enabled']) { + return $mailer; + } + + $retryMailer = new RetryMailer($c->mailLogger, $c->config['mail']); + + // Use UF debug settings to override any service-specific log settings. + if (!$c->config['debug.smtp']) { + $retryMailer->getPhpMailer()->SMTPDebug = 0; + } + + return $retryMailer; + }); } } \ No newline at end of file