* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE * * Sends email notifications on backup success or failure. * Uses Joomla's built-in mail system (Factory::getMailer()). */ namespace Joomla\Component\MokoBackup\Administrator\Engine; defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; class NotificationSender { /** * Send a backup notification email. * * @param object $profile Profile object with notification settings * @param object $record Backup record object with results * @param bool $success Whether the backup succeeded * @param string $logText Backup log text * * @return bool True if email was sent */ public static function send(object $profile, object $record, bool $success, string $logText = ''): bool { $notifyEmail = trim($profile->notify_email ?? ''); $notifyUserGroups = $profile->notify_user_groups ?? ''; // Resolve user group members to email addresses $groupEmails = self::resolveUserGroupEmails($notifyUserGroups); if (empty($notifyEmail) && empty($groupEmails)) { return false; } // Check notification preferences if ($success && empty($profile->notify_on_success)) { return false; } if (!$success && empty($profile->notify_on_failure)) { return false; } try { $mailer = Factory::getMailer(); $config = Factory::getApplication()->getConfig(); $siteName = $config->get('sitename', 'Joomla Site'); $siteUrl = Uri::root(); // Parse recipient list (comma-separated) + user group emails $recipients = array_map('trim', explode(',', $notifyEmail)); $recipients = array_merge($recipients, $groupEmails); $recipients = array_unique(array_filter($recipients, fn($e) => filter_var($e, FILTER_VALIDATE_EMAIL))); if (empty($recipients)) { return false; } foreach ($recipients as $recipient) { $mailer->addRecipient($recipient); } // Build subject $statusLabel = $success ? 'SUCCESS' : 'FAILED'; $mailer->setSubject("[MokoBackup] {$statusLabel}: {$record->description} — {$siteName}"); // Build body $duration = ''; if (!empty($record->backupstart) && !empty($record->backupend) && $record->backupend !== '0000-00-00 00:00:00') { $start = strtotime($record->backupstart); $end = strtotime($record->backupend); $seconds = max(0, $end - $start); $duration = $seconds < 60 ? $seconds . ' seconds' : round($seconds / 60, 1) . ' minutes'; } $sizeHuman = $record->total_size > 0 ? number_format($record->total_size / 1048576, 2) . ' MB' : 'N/A'; $body = "MokoJoomBackup Notification\n" . "============================\n\n" . "Site: {$siteName}\n" . "URL: {$siteUrl}\n" . "Status: {$statusLabel}\n" . "Profile: {$profile->title}\n" . "Description: {$record->description}\n" . "Type: {$record->backup_type}\n" . "Origin: {$record->origin}\n" . "Archive: {$record->archivename}\n" . "Size: {$sizeHuman}\n" . "Duration: {$duration}\n" . "Started: {$record->backupstart}\n" . "Ended: {$record->backupend}\n"; if (!empty($record->remote_filename)) { $body .= "Remote: {$record->remote_filename}\n"; } if ($record->files_count > 0 || $record->tables_count > 0) { $body .= "Files: {$record->files_count}\n" . "Tables: {$record->tables_count}\n"; } // Add log excerpt on failure (last 30 lines) if (!$success && !empty($logText)) { $logLines = explode("\n", $logText); $excerpt = array_slice($logLines, -30); $body .= "\n--- Log (last 30 lines) ---\n" . implode("\n", $excerpt) . "\n"; } $body .= "\n--\n" . "MokoJoomBackup — https://mokoconsulting.tech\n"; $mailer->setBody($body); $mailer->isHtml(false); return $mailer->Send(); } catch (\Throwable $e) { // Don't let notification failure break the backup flow error_log('MokoBackup notification error: ' . $e->getMessage()); return false; } } /** * Resolve user group IDs to email addresses of group members. * * @param string|array $groups Comma-separated group IDs or array * * @return array Email addresses */ private static function resolveUserGroupEmails(string|array $groups): array { if (empty($groups)) { return []; } if (\is_string($groups)) { $groups = array_filter(array_map('intval', explode(',', $groups))); } if (empty($groups)) { return []; } try { $db = Factory::getDbo(); $query = $db->getQuery(true) ->select('DISTINCT ' . $db->quoteName('u.email')) ->from($db->quoteName('#__users', 'u')) ->join('INNER', $db->quoteName('#__user_usergroup_map', 'ugm') . ' ON ugm.user_id = u.id') ->where($db->quoteName('u.block') . ' = 0') ->whereIn($db->quoteName('ugm.group_id'), $groups); $db->setQuery($query); return $db->loadColumn() ?: []; } catch (\Throwable $e) { return []; } } }