first commit
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/mail/Mailer.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class Mailer
|
||||
*
|
||||
* @ingroup mail
|
||||
*
|
||||
* @brief Represents interface to manage emails sending and view
|
||||
*/
|
||||
|
||||
namespace PKP\mail;
|
||||
|
||||
use APP\core\Application;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Mail\Mailer as IlluminateMailer;
|
||||
use Illuminate\Mail\Message;
|
||||
use InvalidArgumentException;
|
||||
use PKP\config\Config;
|
||||
use PKP\observers\events\MessageSendingFromContext;
|
||||
use PKP\observers\events\MessageSendingFromSite;
|
||||
use PKP\site\Site;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\Exception\TransportException;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class Mailer extends IlluminateMailer
|
||||
{
|
||||
/**
|
||||
* Don't bind Laravel View Service, as it's not implemented
|
||||
*
|
||||
* @var null
|
||||
*/
|
||||
protected $views = null;
|
||||
|
||||
/**
|
||||
* Maximum number of notification emails that can be sent per job
|
||||
*/
|
||||
public const BULK_EMAIL_SIZE_LIMIT = 50;
|
||||
|
||||
/**
|
||||
* Creates new Mailer instance without binding with View
|
||||
*
|
||||
* @copydoc \Illuminate\Mail\Mailer::__construct()
|
||||
*/
|
||||
public function __construct(string $name, TransportInterface $transport, Dispatcher $events = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->transport = $transport;
|
||||
$this->events = $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders email content into HTML string
|
||||
*
|
||||
* @param string|object $view
|
||||
* @param array $data variable => value, 'message' is reserved for the Laravel's Swift Message (Illuminate\Mail\Message)
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @see \Illuminate\Mail\Mailer::renderView()
|
||||
*/
|
||||
protected function renderView($view, $data): string
|
||||
{
|
||||
if ($view instanceof Htmlable) {
|
||||
// return HTML without data compiling
|
||||
return $view->toHtml();
|
||||
}
|
||||
|
||||
if (!is_string($view)) {
|
||||
throw new InvalidArgumentException('View must be instance of ' . Htmlable::class . ' or a string, ' . get_class($view) . ' is given');
|
||||
}
|
||||
|
||||
return $this->compileParams($view, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles email templates by substituting variables with their real values
|
||||
*
|
||||
* @param string $view text or HTML string
|
||||
* @param array $data variables with their values passes ['variable' => value]
|
||||
*
|
||||
* @return string compiled string with substitute variables
|
||||
*/
|
||||
public function compileParams(string $view, array $data): string
|
||||
{
|
||||
$variables = [];
|
||||
$replacements = [];
|
||||
foreach ($data as $key => $value) {
|
||||
// Don't compile pre-set message data variables not belonging to the template
|
||||
if (in_array($key, Mailable::getReservedDataKeys())) {
|
||||
continue;
|
||||
}
|
||||
$variables[] = '/\{\$' . $key . '\}/';
|
||||
$replacements[] = $value;
|
||||
}
|
||||
|
||||
return preg_replace($variables, $replacements, $view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc IlluminateMailer::send()
|
||||
*
|
||||
* @param null|mixed $callback
|
||||
*/
|
||||
public function send($view, array $data = [], $callback = null)
|
||||
{
|
||||
if (is_a($view, Mailable::class)) {
|
||||
/** @var Mailable $view */
|
||||
$view->setData();
|
||||
}
|
||||
|
||||
// Application is set to sandbox mode and will sent any emails to log
|
||||
if (Config::getVar('general', 'sandbox', false)) {
|
||||
error_log('Application is set to sandbox mode and will sent any emails to log');
|
||||
}
|
||||
|
||||
parent::send($view, $data, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides Illuminate Mailer method to provide additional parameters to the event
|
||||
*
|
||||
* @param \Symfony\Component\Mime\Email $message
|
||||
* @param array $data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function shouldSendMessage($message, $data = [])
|
||||
{
|
||||
if (!$this->events) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (array_key_exists(Mailable::DATA_KEY_CONTEXT, $data)) {
|
||||
$context = $data[Mailable::DATA_KEY_CONTEXT];
|
||||
return $this->events->until(new MessageSendingFromContext($context, $message, $data)) !== false;
|
||||
}
|
||||
|
||||
$site = Application::get()->getRequest()->getSite();
|
||||
return $this->events->until(new MessageSendingFromSite($site, $message, $data)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override method to catch an exception while sending email instance
|
||||
*
|
||||
* @return \Symfony\Component\Mailer\SentMessage|null
|
||||
*/
|
||||
protected function sendSymfonyMessage(Email $message)
|
||||
{
|
||||
$sentMessage = null;
|
||||
try {
|
||||
$sentMessage = $this->transport->send($message, Envelope::create($message));
|
||||
} catch (TransportException $e) {
|
||||
error_log($e->getMessage());
|
||||
}
|
||||
|
||||
return $sentMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides Illuminate Mailer method to modify email header
|
||||
*
|
||||
* @copydoc Illuminate\Mail\Mailer::addContent()
|
||||
*/
|
||||
protected function addContent($message, $view, $plain, $raw, $data): void
|
||||
{
|
||||
parent::addContent($message, $view, $plain, $raw, $data);
|
||||
|
||||
$this->setEnvelopeSenderDefault($message, $data);
|
||||
$this->setDmarcCompliantFrom($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets envelope sender, either the default one or from the context settings
|
||||
*/
|
||||
protected function setEnvelopeSenderDefault(Message $message, array $data): void
|
||||
{
|
||||
// Force default site-wide envelope sender if set
|
||||
$configDefaultEnvelopeSender = Config::getVar('email', 'default_envelope_sender');
|
||||
if (Config::getVar('email', 'force_default_envelope_sender') && $configDefaultEnvelopeSender) {
|
||||
$message->sender($configDefaultEnvelopeSender);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't provide further checks if envelope sender isn't allowed in the config
|
||||
if (!Config::getVar('email', 'allow_envelope_sender')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the sender provided in the context settings
|
||||
$context = $data[Mailable::DATA_KEY_CONTEXT] ?? null;
|
||||
if ($context && $sender = $context->getData('envelopeSender')) {
|
||||
$message->sender($sender);
|
||||
return;
|
||||
}
|
||||
|
||||
// Finally, provide default sender from the config if not specified
|
||||
if (!$message->getSender() && $configDefaultEnvelopeSender) {
|
||||
$message->sender($configDefaultEnvelopeSender);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set DMARC compliant From header field body
|
||||
*/
|
||||
protected function setDmarcCompliantFrom(Message $message): void
|
||||
{
|
||||
if (empty($message->getFrom())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(
|
||||
Config::getVar('email', 'force_default_envelope_sender')
|
||||
&& Config::getVar('email', 'default_envelope_sender')
|
||||
&& Config::getVar('email', 'force_dmarc_compliant_from')
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->promoteFromToReplyTo($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a DMARC compliant RFC5322.From was requested we need to promote the original RFC5322. From into a Reply-to header
|
||||
* and then munge the RFC5322.From
|
||||
*/
|
||||
protected function promoteFromToReplyTo(Message $message): void
|
||||
{
|
||||
$replyToEmails = array_map(fn ($x) => $x->getAddress(), $message->getReplyTo());
|
||||
$fromEmails = array_map(fn ($x) => $x->getAddress(), $message->getFrom());
|
||||
$alreadyExists = array_intersect($replyToEmails, $fromEmails);
|
||||
|
||||
foreach ($message->getFrom() as $address) {
|
||||
if (!in_array($address->getAddress(), $alreadyExists)) {
|
||||
$message->addReplyTo($address);
|
||||
}
|
||||
}
|
||||
|
||||
$site = Application::get()->getRequest()->getSite(); /** @var Site $site **/
|
||||
$dmarcFromName = '';
|
||||
if (Config::getVar('email', 'dmarc_compliant_from_displayname')) {
|
||||
$patterns = ['#%n#', '#%s#'];
|
||||
$replacements = [
|
||||
implode(',', array_map(fn ($x) => $x->getName(), $message->getFrom())),
|
||||
$site->getLocalizedData('title'),
|
||||
];
|
||||
$dmarcFromName = preg_replace($patterns, $replacements, Config::getVar('email', 'dmarc_compliant_from_displayname'));
|
||||
}
|
||||
|
||||
$message->from(Config::getVar('email', 'default_envelope_sender'), $dmarcFromName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user