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, ' . ($view === null ? 'null' : 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($view->getLocale()); } // Application is set to sandbox mode and will send any emails to log if (Config::getVar('general', 'sandbox', false)) { error_log('Application is set to sandbox mode and will send any emails to the 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) { /* ---------- Key fix: Force rewrite From again before sending ---------- */ $this->setDmarcCompliantFrom($message); /* -------------------------------------------------------------------- */ $sentMessage = null; try { Hook::call('Email::send::before', ['message' => $message, 'mailer' => $this]); $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 * * @param mixed $message Illuminate\Mail\Message | Symfony\Component\Mime\Email */ protected function setDmarcCompliantFrom($message): void { // (Keep if/early‑exit will skip in addContent stage, then forced by sendSymfonyMessage) 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); } /** * Promote original From into Reply‑To and rewrite From * * @param mixed $message Illuminate\Mail\Message | Symfony\Component\Mime\Email */ protected function promoteFromToReplyTo($message): void { /* --------- Uniformly extract existing addresses --------- */ $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)) { if ($message instanceof Message) { $message->addReplyTo($address); } else { // Symfony Email $current = $message->getReplyTo(); $current[] = $address; $message->replyTo(...$current); } } } /** @var Site $site */ $site = Application::get()->getRequest()->getSite(); $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')); } $defaultAddress = Config::getVar('email', 'default_envelope_sender'); if ($message instanceof Message) { $message->from($defaultAddress, $dmarcFromName); } else { // Symfony Email $symAddress = new Address($defaultAddress, $dmarcFromName); $message->from($symAddress); } } }