From 1f4471e088a9cf08edd6ae519114404d102d91e9 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 2 Jun 2026 16:19:31 -0500 Subject: [PATCH] feat: email notifications on backup success/failure (#14) - NotificationSender: sends email via Joomla's mailer on backup complete or fail, with site info, duration, size, and log excerpt - Per-profile settings: notify_email (comma-separated), notify_on_success, notify_on_failure (default: on) - Notifications tab added to profile editor - Wired into BackupEngine for both success and failure paths - Failure emails include last 30 lines of backup log for debugging Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/com_mokobackup/forms/profile.xml | 33 +++++ .../language/en-GB/com_mokobackup.ini | 10 ++ .../com_mokobackup/sql/install.mysql.sql | 3 + .../src/Engine/BackupEngine.php | 19 ++- .../src/Engine/NotificationSender.php | 136 ++++++++++++++++++ .../com_mokobackup/tmpl/profile/edit.php | 8 ++ 6 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 src/packages/com_mokobackup/src/Engine/NotificationSender.php diff --git a/src/packages/com_mokobackup/forms/profile.xml b/src/packages/com_mokobackup/forms/profile.xml index 85d6354..e187491 100644 --- a/src/packages/com_mokobackup/forms/profile.xml +++ b/src/packages/com_mokobackup/forms/profile.xml @@ -158,6 +158,39 @@ +
+ + + + + + + + + +
+
updateObject('#__mokobackup_records', $update, 'id'); + // Send success notification + NotificationSender::send($profile, $update, true, implode("\n", $this->log)); + return [ 'success' => true, 'message' => 'Backup complete: ' . $archiveName . ' (' . $sizeHuman . ')', @@ -219,14 +222,22 @@ class BackupEngine $this->log('FATAL: ' . $e->getMessage()); $update = (object) [ - 'id' => $recordId, - 'status' => 'fail', - 'backupend' => date('Y-m-d H:i:s'), - 'log' => implode("\n", $this->log), + 'id' => $recordId, + 'status' => 'fail', + 'description' => $description ?: '', + 'backup_type' => $profile->backup_type ?? 'full', + 'origin' => $origin, + 'archivename' => $archiveName, + 'backupstart' => $now ?? date('Y-m-d H:i:s'), + 'backupend' => date('Y-m-d H:i:s'), + 'log' => implode("\n", $this->log), ]; $db->updateObject('#__mokobackup_records', $update, 'id'); + // Send failure notification + NotificationSender::send($profile, $update, false, implode("\n", $this->log)); + return ['success' => false, 'message' => 'Backup failed: ' . $e->getMessage(), 'record_id' => $recordId]; } } diff --git a/src/packages/com_mokobackup/src/Engine/NotificationSender.php b/src/packages/com_mokobackup/src/Engine/NotificationSender.php new file mode 100644 index 0000000..82808a0 --- /dev/null +++ b/src/packages/com_mokobackup/src/Engine/NotificationSender.php @@ -0,0 +1,136 @@ + + * @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 ?? ''); + + if (empty($notifyEmail)) { + 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) + $recipients = array_map('trim', explode(',', $notifyEmail)); + $recipients = 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; + } + } +} diff --git a/src/packages/com_mokobackup/tmpl/profile/edit.php b/src/packages/com_mokobackup/tmpl/profile/edit.php index 6a4e3ee..aa73556 100644 --- a/src/packages/com_mokobackup/tmpl/profile/edit.php +++ b/src/packages/com_mokobackup/tmpl/profile/edit.php @@ -50,6 +50,14 @@ HTMLHelper::_('behavior.keepalive'); + +
+
+ form->renderFieldset('notifications'); ?> +
+
+ +