* @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Mail; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\File; use Joomla\CMS\Filesystem\Path; use Joomla\CMS\Language\Text; use Joomla\CMS\Mail\Exception\MailDisabledException; use Joomla\Database\ParameterType; use Joomla\Registry\Registry; use PHPMailer\PHPMailer\Exception as phpmailerException; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Email Templating Class * * @since 4.0.0 */ class MailTemplate { /** * Mailer object to send the actual mail. * * @var \Joomla\CMS\Mail\Mail * @since 4.0.0 */ protected $mailer; /** * Identifier of the mail template. * * @var string * @since 4.0.0 */ protected $template_id; /** * Language of the mail template. * * @var string */ protected $language; /** * * @var string[] * @since 4.0.0 */ protected $data = array(); /** * * @var string[] * @since 4.0.0 */ protected $attachments = array(); /** * List of recipients of the email * * @var \stdClass[] * @since 4.0.0 */ protected $recipients = array(); /** * Reply To of the email * * @var \stdClass * @since 4.0.0 */ protected $replyto; /** * Constructor for the mail templating class * * @param string $templateId Id of the mail template. * @param string $language Language of the template to use. * @param Mail $mailer Mail object to send the mail with. * * @since 4.0.0 */ public function __construct($templateId, $language, Mail $mailer = null) { $this->template_id = $templateId; $this->language = $language; if ($mailer) { $this->mailer = $mailer; } else { $this->mailer = Factory::getMailer(); } } /** * Add an attachment to the mail * * @param string $name Filename of the attachment * @param string $file Either a filepath or filecontent * * @return void * * @since 4.0.0 */ public function addAttachment($name, $file) { $attachment = new \stdClass(); $attachment->name = $name; $attachment->file = $file; $this->attachments[] = $attachment; } /** * Adds recipients for this mail * * @param string $mail Mail address of the recipient * @param string $name Name of the recipient * @param string $type How should the recipient receive the mail? ('to', 'cc', 'bcc') * * @return void * * @since 4.0.0 */ public function addRecipient($mail, $name = null, $type = 'to') { $recipient = new \stdClass(); $recipient->mail = $mail; $recipient->name = $name ?? $mail; $recipient->type = $type; $this->recipients[] = $recipient; } /** * Set reply to for this mail * * @param string $mail Mail address to reply to * @param string $name Name * * @return void * * @since 4.0.0 */ public function setReplyTo($mail, $name = '') { $reply = new \stdClass(); $reply->mail = $mail; $reply->name = $name; $this->replyto = $reply; } /** * Add data to replace in the template * * @param array $data Associative array of strings to replace * * @return void * * @since 4.0.0 */ public function addTemplateData($data) { $this->data = array_merge($this->data, $data); } /** * Render and send the mail * * @return boolean True on success * * @since 4.0.0 * @throws \Exception * @throws MailDisabledException * @throws phpmailerException */ public function send() { $config = ComponentHelper::getParams('com_mails'); $mail = self::getTemplate($this->template_id, $this->language); // If the Mail Template was not found in the db, we cannot send an email. if ($mail === null) { return false; } /** @var Registry $params */ $params = $mail->params; $app = Factory::getApplication(); if ((int) $config->get('alternative_mailconfig', 0) === 1 && (int) $params->get('alternative_mailconfig', 0) === 1) { if ($this->mailer->Mailer === 'smtp' || $params->get('mailer') === 'smtp') { $smtpauth = ($params->get('smtpauth', $app->get('smtpauth')) == 0) ? null : 1; $smtpuser = $params->get('smtpuser', $app->get('smtpuser')); $smtppass = $params->get('smtppass', $app->get('smtppass')); $smtphost = $params->get('smtphost', $app->get('smtphost')); $smtpsecure = $params->get('smtpsecure', $app->get('smtpsecure')); $smtpport = $params->get('smtpport', $app->get('smtpport')); $this->mailer->useSmtp($smtpauth, $smtphost, $smtpuser, $smtppass, $smtpsecure, $smtpport); } if ($params->get('mailer') === 'sendmail') { $this->mailer->isSendmail(); } $mailfrom = $params->get('mailfrom', $app->get('mailfrom')); $fromname = $params->get('fromname', $app->get('fromname')); if (MailHelper::isEmailAddress($mailfrom)) { $this->mailer->setFrom(MailHelper::cleanLine($mailfrom), MailHelper::cleanLine($fromname), false); } } $app->triggerEvent('onMailBeforeRendering', array($this->template_id, &$this)); $subject = $this->replaceTags(Text::_($mail->subject), $this->data); $this->mailer->setSubject($subject); $mailStyle = $config->get('mail_style', 'plaintext'); $plainBody = $this->replaceTags(Text::_($mail->body), $this->data); $htmlBody = $this->replaceTags(Text::_($mail->htmlbody), $this->data); if ($mailStyle === 'plaintext' || $mailStyle === 'both') { // If the Plain template is empty try to convert the HTML template to a Plain text if (!$plainBody) { $plainBody = strip_tags(str_replace(['
', '
', '
'], "\n", $htmlBody)); } $this->mailer->setBody($plainBody); // Set alt body, use $mailer->Body directly because it was filtered by $mailer->setBody() if ($mailStyle === 'both') { $this->mailer->AltBody = $this->mailer->Body; } } if ($mailStyle === 'html' || $mailStyle === 'both') { $this->mailer->isHtml(true); // If HTML body is empty try to convert the Plain template to html if (!$htmlBody) { $htmlBody = nl2br($plainBody, false); } $htmlBody = MailHelper::convertRelativeToAbsoluteUrls($htmlBody); $this->mailer->setBody($htmlBody); } if ($config->get('copy_mails') && $params->get('copyto')) { $this->mailer->addBcc($params->get('copyto')); } foreach ($this->recipients as $recipient) { switch ($recipient->type) { case 'cc': $this->mailer->addCc($recipient->mail, $recipient->name); break; case 'bcc': $this->mailer->addBcc($recipient->mail, $recipient->name); break; case 'to': default: $this->mailer->addAddress($recipient->mail, $recipient->name); } } if ($this->replyto) { $this->mailer->addReplyTo($this->replyto->mail, $this->replyto->name); } if (trim($config->get('attachment_folder', ''))) { $folderPath = rtrim(Path::check(JPATH_ROOT . '/' . $config->get('attachment_folder')), \DIRECTORY_SEPARATOR); if ($folderPath && $folderPath !== Path::clean(JPATH_ROOT) && is_dir($folderPath)) { foreach ((array) json_decode($mail->attachments) as $attachment) { $filePath = Path::check($folderPath . '/' . $attachment->file); if (is_file($filePath)) { $this->mailer->addAttachment($filePath, $this->getAttachmentName($filePath, $attachment->name)); } } } } foreach ($this->attachments as $attachment) { if (is_file($attachment->file)) { $this->mailer->addAttachment($attachment->file, $this->getAttachmentName($attachment->file, $attachment->name)); } else { $this->mailer->addStringAttachment($attachment->file, $attachment->name); } } return $this->mailer->Send(); } /** * Replace tags with their values recursively * * @param string $text The template to process * @param array $tags An associative array to replace in the template * * @return string Rendered mail template * * @since 4.0.0 */ protected function replaceTags($text, $tags) { foreach ($tags as $key => $value) { if (is_array($value)) { $matches = array(); if (preg_match_all('/{' . strtoupper($key) . '}(.*?){\/' . strtoupper($key) . '}/s', $text, $matches)) { foreach ($matches[0] as $i => $match) { $replacement = ''; foreach ($value as $subvalue) { if (is_array($subvalue)) { $replacement .= $this->replaceTags($matches[1][$i], $subvalue); } } $text = str_replace($match, $replacement, $text); } } } else { $text = str_replace('{' . strtoupper($key) . '}', $value, $text); } } return $text; } /** * Get a specific mail template * * @param string $key Template identifier * @param string $language Language code of the template * * @return object|null An object with the data of the mail, or null if the template not found in the db. * * @since 4.0.0 */ public static function getTemplate($key, $language) { $db = Factory::getDbo(); $query = $db->getQuery(true); $query->select('*') ->from($db->quoteName('#__mail_templates')) ->where($db->quoteName('template_id') . ' = :key') ->whereIn($db->quoteName('language'), ['', $language], ParameterType::STRING) ->order($db->quoteName('language') . ' DESC') ->bind(':key', $key); $db->setQuery($query); $mail = $db->loadObject(); if ($mail) { $mail->params = new Registry($mail->params); } return $mail; } /** * Insert a new mail template into the system * * @param string $key Mail template key * @param string $subject A default subject (normally a translatable string) * @param string $body A default body (normally a translatable string) * @param array $tags Associative array of tags to replace * @param string $htmlbody A default htmlbody (normally a translatable string) * * @return boolean True on success, false on failure * * @since 4.0.0 */ public static function createTemplate($key, $subject, $body, $tags, $htmlbody = '') { $db = Factory::getDbo(); $template = new \stdClass(); $template->template_id = $key; $template->language = ''; $template->subject = $subject; $template->body = $body; $template->htmlbody = $htmlbody; $template->attachments = ''; $params = new \stdClass(); $params->tags = array($tags); $template->params = json_encode($params); return $db->insertObject('#__mail_templates', $template); } /** * Update an existing mail template * * @param string $key Mail template key * @param string $subject A default subject (normally a translatable string) * @param string $body A default body (normally a translatable string) * @param array $tags Associative array of tags to replace * @param string $htmlbody A default htmlbody (normally a translatable string) * * @return boolean True on success, false on failure * * @since 4.0.0 */ public static function updateTemplate($key, $subject, $body, $tags, $htmlbody = '') { $db = Factory::getDbo(); $template = new \stdClass(); $template->template_id = $key; $template->language = ''; $template->subject = $subject; $template->body = $body; $template->htmlbody = $htmlbody; $params = new \stdClass(); $params->tags = array($tags); $template->params = json_encode($params); return $db->updateObject('#__mail_templates', $template, ['template_id', 'language']); } /** * Method to delete a mail template * * @param string $key The key of the mail template * * @return boolean True on success, false on failure * * @since 4.0.0 */ public static function deleteTemplate($key) { $db = Factory::getDbo(); $query = $db->getQuery(true); $query->delete($db->quoteName('#__mail_templates')) ->where($db->quoteName('template_id') . ' = :key') ->bind(':key', $key); $db->setQuery($query); return $db->execute(); } /** * Check and if necessary fix the file name of an attachment so that the attached file * has the same extension as the source file, and not a different file extension * * @param string $file Path to the file to be attached * @param string $name The file name to be used for the attachment * * @return string The corrected file name for the attachment * * @since 4.0.0 */ protected function getAttachmentName(string $file, string $name): string { // If no name is given, do not process it further if (!trim($name)) { return ''; } // Replace any placeholders. $name = $this->replaceTags($name, $this->data); // Get the file extension. $ext = File::getExt($file); // Strip off extension from $name and append extension of $file, if any return File::stripExt($name) . ($ext ? '.' . $ext : ''); } }