* @link https://www.tassos.gr * @copyright Copyright © 2024 Tassos All Rights Reserved * @license GNU GPLv3 or later */ namespace NRFramework; defined('_JEXEC') or die('Restricted access'); use NRFramework\Functions; use NRFramework\URLHelper; use Joomla\CMS\Factory; use Joomla\CMS\Uri\Uri; use Joomla\Filesystem\Path; use Joomla\CMS\Language\Text; /** * Framework Emailer */ class Email { /** * Indicates the last error * * @var string */ public $error; /** * Email Object * * @var email data to be sent */ private $email; /** * Required elements for a valid email object * * @var array */ private $requiredKeys = [ 'from_email', 'from_name', 'recipient', 'subject', 'body' ]; /** * Class constructor */ public function __construct($email) { $this->email = $email; } /** * Validates Email Object * * @param array $email The email object * * @return boolean Returns true if the email object is valid */ public function validate() { // Validate email object if (!$this->email || !is_array($this->email) || !count($this->email)) { $this->setError('Invalid email object.'); return; } // Check for missing properties foreach ($this->requiredKeys as $key) { if (!isset($this->email[$key]) || empty($this->email[$key])) { $this->setError("The $key field is either missing or invalid."); return; } } // Validate recipient email addresses. $this->email['recipient'] = Functions::makeArray($this->email['recipient']); foreach ($this->email['recipient'] as $recipient) { if (!$this->validateEmailAddress($recipient)) { $this->setError("Invalid recipient email address: $recipient"); return; } } // Validate sender email address if (!$this->validateEmailAddress($this->email['from_email'])) { $this->setError('Invalid sender email address: ' . $this->email['from_email']); return; } $this->email['bcc'] = isset($this->email['bcc']) ? Functions::makeArray($this->email['bcc']) : []; $this->email['cc'] = isset($this->email['cc']) ? Functions::makeArray($this->email['cc']) : []; // Convert special HTML entities back to characters on non text-only properties. // For instance, the subject line of an email is not parsed as HTML, it's just pure text. // Because of this an HTML entity like & it will be displayed as encoded. // To prevent this from happening we need decode the values. $this->email['subject'] = htmlspecialchars_decode($this->email['subject']); $this->email['from_name'] = htmlspecialchars_decode($this->email['from_name']); $this->email['reply_to_name'] = htmlspecialchars_decode($this->email['reply_to_name']); return true; } /** * Sending emails * * @param array $email The mail objecta * * @return mixed Returns true on success. Throws exeption on fail. */ public function send() { // Proceed only if Mail Sending is enabled. if (!Factory::getConfig()->get('mailonline')) { $this->error = Text::_('NR_ERROR_EMAIL_IS_DISABLED'); return; } // Validate first the email object if (!$this->validate($this->email)) { return; } $email = $this->email; $mailer = Factory::getMailer(); $mailer->CharSet = 'UTF-8'; $mailer->Encoding = 'quoted-printable'; // Email Sender $mailer->setSender([ $email['from_email'], $email['from_name'] ]); // Reply-to if (isset($email['reply_to']) && !empty($email['reply_to'])) { $name = (isset($email['reply_to_name']) && !empty($email['reply_to_name'])) ? $email['reply_to_name'] : ''; $reply_to_addresses = array_filter(array_map('trim', explode(',', $email['reply_to']))); foreach($reply_to_addresses as $reply_to_address) { $mailer->addReplyTo($reply_to_address, $name); } } // Convert all relative paths found in and elements to absolute URLs $email['body'] = URLHelper::relativePathsToAbsoluteURLs($email['body']); // Fix space characters displayed as ???? in old email clients like SquirrelMail. // Ticket reference: https://smilemotive.teamwork.com/desk/tickets/96313487/messages $specialSpace = [ "\xC2\xA0", "\xE1\xA0\x8E", "\xE2\x80\x80", "\xE2\x80\x81", "\xE2\x80\x82", "\xE2\x80\x83", "\xE2\x80\x84", "\xE2\x80\x85", "\xE2\x80\x86", "\xE2\x80\x87", "\xE2\x80\x88", "\xE2\x80\x89", "\xE2\x80\x8A", "\xE2\x80\x8B", "\xE2\x80\xAF", "\xE2\x81\x9F", "\xEF\xBB\xBF", ]; $email['body'] = str_replace($specialSpace, " ", $email['body']); $mailer ->addRecipient($email['recipient']) ->isHTML(true) ->setSubject($email['subject']) ->setBody($email['body']); $mailer->AltBody = strip_tags(str_ireplace(['
', '
', '
'], "\r\n", $email['body'])); // Add CC if (!empty($email['cc'])) { $mailer->addCc($email['cc']); } // Add BCC if (!empty($email['bcc'])) { $mailer->addBcc($email['bcc']); } // Attachments $attachments = $email['attachments']; if (!empty($attachments)) { if (!is_array($attachments)) { $attachments = explode(',', $attachments); } // Validate Attachments $attachments = array_filter(array_map('trim', $attachments)); foreach ($attachments as $attachment) { $file_path = $this->toRelativePath($attachment); if (!is_file($file_path)) { continue; } $mailer->addAttachment($file_path); } } // Send mail $send = $mailer->Send(); if ($send !== true) { $this->setError($send->__toString()); return; } return true; } /** * Set Class Error * * @param string $error The error message */ private function setError($error) { $this->error = 'Error sending email: ' . $error; } /** * Removes all illegal characters and validates an email address * * @param string $email Email address string * * @return bool */ private function validateEmailAddress($email) { // If the email address contains an ampersand, throw an error if (strpos($email, '&') !== false) { return false; } $email = filter_var($email, FILTER_SANITIZE_EMAIL); return filter_var($email, FILTER_VALIDATE_EMAIL); } /** * Attempts to transform an absolute URL to path relative to the site's root. * * @param string $url * * @return string */ private function toRelativePath($url) { $needles = [ Uri::root(), JPATH_SITE, JPATH_ROOT ]; $path = str_replace($needles, '', $url); $path = Path::clean($path); // Relative paths should not start with a slash. $path = ltrim($path, '/'); return $path; } }