feat: email notifications on backup success/failure (#14)
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -158,6 +158,39 @@
|
||||
</field>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="notifications" label="COM_MOKOBACKUP_FIELDSET_NOTIFICATIONS">
|
||||
<field
|
||||
name="notify_email"
|
||||
type="text"
|
||||
label="COM_MOKOBACKUP_FIELD_NOTIFY_EMAIL"
|
||||
description="COM_MOKOBACKUP_FIELD_NOTIFY_EMAIL_DESC"
|
||||
maxlength="512"
|
||||
hint="admin@example.com, backup@example.com"
|
||||
/>
|
||||
<field
|
||||
name="notify_on_success"
|
||||
type="radio"
|
||||
label="COM_MOKOBACKUP_FIELD_NOTIFY_SUCCESS"
|
||||
description="COM_MOKOBACKUP_FIELD_NOTIFY_SUCCESS_DESC"
|
||||
default="0"
|
||||
class="btn-group"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field
|
||||
name="notify_on_failure"
|
||||
type="radio"
|
||||
label="COM_MOKOBACKUP_FIELD_NOTIFY_FAILURE"
|
||||
description="COM_MOKOBACKUP_FIELD_NOTIFY_FAILURE_DESC"
|
||||
default="1"
|
||||
class="btn-group"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="ftp" label="COM_MOKOBACKUP_FIELDSET_FTP">
|
||||
<field
|
||||
name="ftp_host"
|
||||
|
||||
@@ -152,6 +152,16 @@ COM_MOKOBACKUP_BACKUP_PROFILE="Backup Profile"
|
||||
COM_MOKOBACKUP_TOOLBAR_RESTORE="Restore"
|
||||
COM_MOKOBACKUP_RESTORE_CONFIRM="WARNING: Restoring will overwrite your current site files and/or database. Are you sure you want to continue?"
|
||||
|
||||
; Notifications
|
||||
COM_MOKOBACKUP_TAB_NOTIFICATIONS="Notifications"
|
||||
COM_MOKOBACKUP_FIELDSET_NOTIFICATIONS="Email Notifications"
|
||||
COM_MOKOBACKUP_FIELD_NOTIFY_EMAIL="Notification Email(s)"
|
||||
COM_MOKOBACKUP_FIELD_NOTIFY_EMAIL_DESC="Comma-separated list of email addresses to notify. Leave empty to disable notifications."
|
||||
COM_MOKOBACKUP_FIELD_NOTIFY_SUCCESS="Notify on Success"
|
||||
COM_MOKOBACKUP_FIELD_NOTIFY_SUCCESS_DESC="Send an email when a backup completes successfully."
|
||||
COM_MOKOBACKUP_FIELD_NOTIFY_FAILURE="Notify on Failure"
|
||||
COM_MOKOBACKUP_FIELD_NOTIFY_FAILURE_DESC="Send an email when a backup fails. Includes log excerpt for debugging."
|
||||
|
||||
; Akeeba Import
|
||||
COM_MOKOBACKUP_TOOLBAR_IMPORT_AKEEBA="Import from Akeeba"
|
||||
COM_MOKOBACKUP_AKEEBA_NOT_FOUND="Akeeba Backup tables not found. Is Akeeba Backup Pro installed?"
|
||||
|
||||
@@ -24,6 +24,9 @@ CREATE TABLE IF NOT EXISTS `#__mokobackup_profiles` (
|
||||
`gdrive_folder_id` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`remote_keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload',
|
||||
`include_kickstart` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include standalone restore.php in archive',
|
||||
`notify_email` VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'Comma-separated notification emails',
|
||||
`notify_on_success` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`notify_on_failure` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`published` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`ordering` INT(11) NOT NULL DEFAULT 0,
|
||||
`created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
|
||||
@@ -210,6 +210,9 @@ class BackupEngine
|
||||
|
||||
$db->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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokobackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,14 @@ HTMLHelper::_('behavior.keepalive');
|
||||
</div>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'profileTab', 'notifications', Text::_('COM_MOKOBACKUP_TAB_NOTIFICATIONS')); ?>
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<?php echo $this->form->renderFieldset('notifications'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'profileTab', 'remote', Text::_('COM_MOKOBACKUP_TAB_REMOTE')); ?>
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
|
||||
Reference in New Issue
Block a user