';
$ctx->render('Confirm deletion', $body);
diff --git a/exec/handlers/index.php b/exec/handlers/index.php
index ad1a9b0..75c2b35 100644
--- a/exec/handlers/index.php
+++ b/exec/handlers/index.php
@@ -24,8 +24,8 @@ return static function (AppContext $ctx): void {
$enabled = !empty($rule['enabled']);
$rows .= '
| ' . AppContext::e($ctx->t($enabled ? 'Enabled' : 'Disabled')) . ' | ';
$rows .= '' . AppContext::e((string)$rule['subject']) . ' ' . date('Y-m-d H:i', (int)($rule['updated_at'] ?? time())) . ' | ';
- $rows .= '' . AppContext::e(date('Y-m-d H:i', (int)$rule['start_ts'])) . ' do ' . AppContext::e(date('Y-m-d H:i', (int)$rule['end_ts'])) . ' | ';
- $rows .= '' . AppContext::e($rule['dynamic_scope'] ? 'Dynamiczny' : 'Snapshot') . ' | ';
+ $rows .= ' | ' . AppContext::e(date('Y-m-d H:i', (int)$rule['start_ts'])) . ' ' . AppContext::e($ctx->t('To')) . ' ' . AppContext::e(date('Y-m-d H:i', (int)$rule['end_ts'])) . ' | ';
+ $rows .= '' . AppContext::e($ctx->t($rule['dynamic_scope'] ? 'Dynamic' : 'Snapshot')) . ' | ';
$rows .= '' . AppContext::e($ctx->t('Edit')) . '';
$rows .= '' . AppContext::e($ctx->t('Preview')) . '';
$rows .= '';
@@ -35,6 +35,6 @@ return static function (AppContext $ctx): void {
$body = '' .
- ' | Status | ' . AppContext::e($ctx->t('Subject')) . ' | Okres wysyłki | Zakres | Akcje | ' . $rows . '
';
+ '| ' . AppContext::e($ctx->t('Status')) . ' | ' . AppContext::e($ctx->t('Subject')) . ' | ' . AppContext::e($ctx->t('Sending period')) . ' | ' . AppContext::e($ctx->t('Scope')) . ' | ' . AppContext::e($ctx->t('Actions')) . ' | ' . $rows . ' ';
$ctx->render('Global autoresponders', $body, $ok);
};
diff --git a/exec/handlers/preview.php b/exec/handlers/preview.php
index 2f43ddb..da53271 100644
--- a/exec/handlers/preview.php
+++ b/exec/handlers/preview.php
@@ -5,12 +5,12 @@ return static function (AppContext $ctx): void {
$id = $ctx->query('id');
$rule = $ctx->rules->find($ctx->daUser->username(), $id);
if ($rule === null) {
- $ctx->render('Preview autoresponder', 'Rule not found ');
+ $ctx->render('Preview autoresponder', '' . AppContext::e($ctx->t('Rule not found')) . ' ');
return;
}
$body = '' . AppContext::e($ctx->t('Preview autoresponder')) . '' . render_preview_box($rule) . '' .
- ' Szczegóły regułyStatus' . AppContext::e($ctx->t(!empty($rule['enabled']) ? 'Enabled' : 'Disabled')) . ' ' .
- ' Okres' . AppContext::e(date('Y-m-d H:i', (int)$rule['start_ts']) . ' - ' . date('Y-m-d H:i', (int)$rule['end_ts'])) . ' ' .
- ' ';
+ '' . AppContext::e($ctx->t('Rule details')) . '' . AppContext::e($ctx->t('Status')) . '' . AppContext::e($ctx->t(!empty($rule['enabled']) ? 'Enabled' : 'Disabled')) . ' ' .
+ ' ' . AppContext::e($ctx->t('Sending period')) . '' . AppContext::e(date('Y-m-d H:i', (int)$rule['start_ts']) . ' - ' . date('Y-m-d H:i', (int)$rule['end_ts'])) . ' ' .
+ ' ';
$ctx->render('Preview autoresponder', $body);
};
diff --git a/exec/handlers/toggle.php b/exec/handlers/toggle.php
index df25faf..d758ed1 100644
--- a/exec/handlers/toggle.php
+++ b/exec/handlers/toggle.php
@@ -7,7 +7,7 @@ return static function (AppContext $ctx): void {
$ctx->requireCsrf('toggle:' . $id);
$rule = $ctx->rules->find($ctx->daUser->username(), $id);
if ($rule === null) {
- throw new RuntimeException('Rule not found');
+ throw new RuntimeException($ctx->t('Rule not found'));
}
$ctx->rules->update($ctx->daUser->username(), $id, ['enabled' => empty($rule['enabled'])]);
Http::redirect($ctx->url('index.html', ['status' => 'toggled']));
diff --git a/exec/handlers/update.php b/exec/handlers/update.php
index 8f580f0..114f6b5 100644
--- a/exec/handlers/update.php
+++ b/exec/handlers/update.php
@@ -5,7 +5,7 @@ return static function (AppContext $ctx): void {
$id = Http::isPost() ? $ctx->post('id') : $ctx->query('id');
$rule = $ctx->rules->find($ctx->daUser->username(), $id);
if ($rule === null) {
- $ctx->render('Edit autoresponder', 'Rule not found ');
+ $ctx->render('Edit autoresponder', '' . AppContext::e($ctx->t('Rule not found')) . ' ');
return;
}
$errors = [];
diff --git a/exec/lib/AppContext.php b/exec/lib/AppContext.php
index e1ee114..2ef86bb 100644
--- a/exec/lib/AppContext.php
+++ b/exec/lib/AppContext.php
@@ -108,14 +108,16 @@ final class AppContext
private function scripts(): string
{
- return <<<'JS'
+ $selectedLabel = json_encode($this->t('Selected'), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
+ return <<backendMode();
+ $current = self::currentBackend($statePath);
+ if ($current === $desired) {
+ return;
+ }
+
+ if (!is_file($backendScript) || !is_executable($backendScript)) {
+ self::log($errorLog, 'Backend script not executable: ' . $backendScript);
+ return;
+ }
+
+ $process = proc_open([$backendScript, $desired], [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes);
+ if (!is_resource($process)) {
+ self::log($errorLog, 'Could not start backend script.');
+ return;
+ }
+ fclose($pipes[0]);
+ stream_get_contents($pipes[1]);
+ $stderr = stream_get_contents($pipes[2]);
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+ $code = proc_close($process);
+ if ($code !== 0) {
+ self::log($errorLog, 'Backend script failed with code ' . $code . ': ' . trim($stderr));
+ }
+ }
+
+ private static function currentBackend(string $statePath): string
+ {
+ if (!is_file($statePath)) {
+ return '';
+ }
+ $data = json_decode((string)file_get_contents($statePath), true);
+ return is_array($data) ? (string)($data['active_backend'] ?? '') : '';
+ }
+
+ private static function log(string $errorLog, string $message): void
+ {
+ if ($errorLog === '') {
+ return;
+ }
+ @error_log('[' . date('c') . '] BACKEND_SYNC ' . $message . "\n", 3, $errorLog);
+ }
+}
diff --git a/exec/lib/RuleValidator.php b/exec/lib/RuleValidator.php
index 7bd4e60..d608669 100644
--- a/exec/lib/RuleValidator.php
+++ b/exec/lib/RuleValidator.php
@@ -53,7 +53,7 @@ final class RuleValidator
{
$value = trim($value);
if ($mode === 'exact_hour') {
- if (!preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:00$/', $value)) {
+ if (!preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}$/', $value)) {
throw new InvalidArgumentException('Nieprawidłowa data lub godzina.');
}
$dt = DateTimeImmutable::createFromFormat('!Y-m-d\TH:i', $value, $tz);
diff --git a/exec/lib/Settings.php b/exec/lib/Settings.php
index 119958f..fbdf9af 100644
--- a/exec/lib/Settings.php
+++ b/exec/lib/Settings.php
@@ -51,7 +51,7 @@ final class Settings
'DEFAULT_PLUGIN_LANGUAGE' => 'en',
'DEFAULT_ENABLE' => 'true',
'GLOBAL_AUTORESPONDER_TIMEZONE' => 'Europe/Warsaw',
- 'GLOBAL_AUTORESPONDER_SCHEDULE_MODE' => 'exact_hour',
+ 'GLOBAL_AUTORESPONDER_BACKEND' => 'da',
'GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE' => 'false',
'GLOBAL_AUTORESPONDER_REPEAT_MINUTES' => '1440',
'GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE' => 'true',
@@ -93,9 +93,14 @@ final class Settings
return $this->getString('GLOBAL_AUTORESPONDER_TIMEZONE', 'Europe/Warsaw');
}
+ public function backendMode(): string
+ {
+ return $this->getString('GLOBAL_AUTORESPONDER_BACKEND', 'da');
+ }
+
public function scheduleMode(): string
{
- return $this->getString('GLOBAL_AUTORESPONDER_SCHEDULE_MODE', 'exact_hour');
+ return $this->backendMode() === 'da' ? 'directadmin_period' : 'exact_hour';
}
public function replyEveryMessage(): bool
@@ -148,8 +153,8 @@ final class Settings
} catch (Throwable) {
throw new InvalidArgumentException('Invalid timezone: ' . $this->timezone());
}
- if (!in_array($this->scheduleMode(), ['exact_hour', 'directadmin_period'], true)) {
- throw new InvalidArgumentException('Invalid schedule mode: ' . $this->scheduleMode());
+ if (!in_array($this->backendMode(), ['da', 'exim'], true)) {
+ throw new InvalidArgumentException('Invalid backend mode: ' . $this->backendMode());
}
if ($this->repeatMinutes() < 0) {
throw new InvalidArgumentException('Invalid repeat minutes');
diff --git a/lang/en.php b/lang/en.php
index e12cfe9..8ea9bc7 100644
--- a/lang/en.php
+++ b/lang/en.php
@@ -12,6 +12,7 @@ return [
'Preview autoresponder' => 'Preview autoresponder',
'Subject' => 'Subject',
'Message body' => 'Message body',
+ 'Response content' => 'Response content',
'Status' => 'Status',
'Enabled' => 'Enabled',
'Disabled' => 'Disabled',
@@ -19,6 +20,11 @@ return [
'End date' => 'End date',
'Start hour' => 'Start hour',
'End hour' => 'End hour',
+ 'Time' => 'Time',
+ 'Time of day' => 'Time of day',
+ 'Morning' => 'Morning',
+ 'Afternoon' => 'Afternoon',
+ 'Evening' => 'Evening',
'Save autoresponder' => 'Save autoresponder',
'Save changes' => 'Save changes',
'Cancel' => 'Cancel',
@@ -36,4 +42,15 @@ return [
'Rule deleted.' => 'Rule deleted.',
'Rule status changed.' => 'Rule status changed.',
'All valid POP/IMAP mailboxes in this account' => 'All valid POP/IMAP mailboxes in this account',
+ 'Rule not found' => 'Rule not found',
+ 'Sending period' => 'Sending period',
+ 'Scope' => 'Scope',
+ 'Actions' => 'Actions',
+ 'Dynamic' => 'Dynamic',
+ 'Snapshot' => 'Snapshot',
+ 'To' => 'to',
+ 'Back' => 'Back',
+ 'Rule details' => 'Rule details',
+ 'Selected' => 'Selected',
+ 'Deleting this rule will disable the automatic reply created by this rule for covered mailboxes.' => 'Deleting this rule will disable the automatic reply created by this rule for covered mailboxes.',
];
diff --git a/lang/pl.php b/lang/pl.php
index 1ded965..097ec4a 100644
--- a/lang/pl.php
+++ b/lang/pl.php
@@ -12,6 +12,7 @@ return [
'Preview autoresponder' => 'Podgląd autorespondera',
'Subject' => 'Temat',
'Message body' => 'Treść wiadomości',
+ 'Response content' => 'Treść odpowiedzi',
'Status' => 'Status',
'Enabled' => 'Włączony',
'Disabled' => 'Wyłączony',
@@ -19,6 +20,11 @@ return [
'End date' => 'Data zakończenia',
'Start hour' => 'Godzina rozpoczęcia',
'End hour' => 'Godzina zakończenia',
+ 'Time' => 'Czas',
+ 'Time of day' => 'Pora dnia',
+ 'Morning' => 'Rano',
+ 'Afternoon' => 'Po południu',
+ 'Evening' => 'Wieczorem',
'Save autoresponder' => 'Zapisz autoresponder',
'Save changes' => 'Zapisz zmiany',
'Cancel' => 'Anuluj',
@@ -36,4 +42,15 @@ return [
'Rule deleted.' => 'Reguła została usunięta.',
'Rule status changed.' => 'Status reguły został zmieniony.',
'All valid POP/IMAP mailboxes in this account' => 'Wszystkie prawidłowe skrzynki POP/IMAP konta',
+ 'Rule not found' => 'Nie znaleziono reguły',
+ 'Sending period' => 'Okres wysyłki',
+ 'Scope' => 'Zakres',
+ 'Actions' => 'Akcje',
+ 'Dynamic' => 'Dynamiczny',
+ 'Snapshot' => 'Migawka',
+ 'To' => 'do',
+ 'Back' => 'Powrót',
+ 'Rule details' => 'Szczegóły reguły',
+ 'Selected' => 'Wybrano',
+ 'Deleting this rule will disable the automatic reply created by this rule for covered mailboxes.' => 'Usunięcie reguły wyłączy odpowiedź automatyczną utworzoną przez tę regułę dla objętych skrzynek.',
];
diff --git a/plugin-settings.conf b/plugin-settings.conf
index 5f4c000..8ddabf1 100644
--- a/plugin-settings.conf
+++ b/plugin-settings.conf
@@ -8,10 +8,10 @@ DEFAULT_ENABLE=true
# Strefa czasu używana do interpretacji i wyświetlania dat rozpoczęcia oraz zakończenia.
GLOBAL_AUTORESPONDER_TIMEZONE=Europe/Warsaw
-# Tryb harmonogramu.
-# exact_hour = wybór dnia i godziny.
-# directadmin_period = wybór dnia oraz morning/afternoon/evening zgodnie z API DirectAdmin.
-GLOBAL_AUTORESPONDER_SCHEDULE_MODE=exact_hour
+# Tryb pracy pluginu.
+# da = użycie natywnych autoresponderów DirectAdmin.
+# exim = własna obsługa z możliwością podania dokładnej godziny i minuty.
+GLOBAL_AUTORESPONDER_BACKEND=da
# Czy odpowiadać automatycznie na każdą kwalifikującą się wiadomość.
GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE=false
diff --git a/plugin.conf b/plugin.conf
index 664b450..7ee32fd 100644
--- a/plugin.conf
+++ b/plugin.conf
@@ -2,6 +2,6 @@ name=global-autoresponder
id=global-autoresponder
type=user
author=HITME.PL
-version=1.0.2
+version=1.0.3
active=no
installed=no
diff --git a/tests/backend_runtime_config_test.php b/tests/backend_runtime_config_test.php
new file mode 100644
index 0000000..a943c89
--- /dev/null
+++ b/tests/backend_runtime_config_test.php
@@ -0,0 +1,21 @@
+> " . escapeshellarg(TEST_TMP . '/called.log') . "\nmkdir -p " . escapeshellarg(TEST_TMP . '/config') . "\nprintf '{\"active_backend\":\"%s\"}\\n' \"$1\" > " . escapeshellarg(TEST_TMP . '/config/backend-state.json') . "\n");
+ chmod($script, 0700);
+
+ BackendRuntimeConfig::sync($settings, $script, TEST_TMP . '/config/backend-state.json');
+ assert_same("exim\n", file_get_contents(TEST_TMP . '/called.log'));
+
+ BackendRuntimeConfig::sync($settings, $script, TEST_TMP . '/config/backend-state.json');
+ assert_same("exim\n", file_get_contents(TEST_TMP . '/called.log'), 'Script should not run again when backend is already active');
+});
diff --git a/tests/bootstrap_test.php b/tests/bootstrap_test.php
index 95ed842..b302d32 100644
--- a/tests/bootstrap_test.php
+++ b/tests/bootstrap_test.php
@@ -10,7 +10,7 @@ test('settings validate defaults and external paths', function (): void {
'DEFAULT_PLUGIN_LANGUAGE=pl',
'DEFAULT_ENABLE=false',
'GLOBAL_AUTORESPONDER_TIMEZONE=Europe/Warsaw',
- 'GLOBAL_AUTORESPONDER_SCHEDULE_MODE=exact_hour',
+ 'GLOBAL_AUTORESPONDER_BACKEND=exim',
'GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE=false',
'GLOBAL_AUTORESPONDER_REPEAT_MINUTES=60',
'GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE=true',
@@ -23,6 +23,7 @@ test('settings validate defaults and external paths', function (): void {
assert_false($settings->defaultEnable());
assert_same('Europe/Warsaw', $settings->timezone());
assert_same('exact_hour', $settings->scheduleMode());
+ assert_same('exim', $settings->backendMode());
assert_same(60, $settings->repeatMinutes());
assert_same('/usr/local/hitme_plugins/global-autoresponders/config', Settings::CONFIG_DIR);
assert_same('/usr/local/hitme_plugins/global-autoresponders/data', Settings::DATA_DIR);
@@ -30,7 +31,7 @@ test('settings validate defaults and external paths', function (): void {
test('settings reject invalid timezone and schedule mode', function (): void {
$path = TEST_TMP . '/bad-settings.conf';
- test_write($path, "GLOBAL_AUTORESPONDER_TIMEZONE=Not/AZone\nGLOBAL_AUTORESPONDER_SCHEDULE_MODE=bad\n");
+ test_write($path, "GLOBAL_AUTORESPONDER_TIMEZONE=Not/AZone\nGLOBAL_AUTORESPONDER_BACKEND=bad\n");
try {
Settings::load($path);
} catch (InvalidArgumentException $e) {
@@ -40,6 +41,19 @@ test('settings reject invalid timezone and schedule mode', function (): void {
throw new RuntimeException('Expected invalid settings to fail');
});
+test('backend mode derives schedule mode', function (): void {
+ $path = TEST_TMP . '/backend.conf';
+ test_write($path, "GLOBAL_AUTORESPONDER_BACKEND=da\n");
+ $settings = Settings::load($path);
+ assert_same('da', $settings->backendMode());
+ assert_same('directadmin_period', $settings->scheduleMode());
+
+ test_write($path, "GLOBAL_AUTORESPONDER_BACKEND=exim\n");
+ $settings = Settings::load($path);
+ assert_same('exim', $settings->backendMode());
+ assert_same('exact_hour', $settings->scheduleMode());
+});
+
test('directadmin user access follows DEFAULT_ENABLE and per-user overrides', function (): void {
$settingsPath = TEST_TMP . '/access.conf';
test_write($settingsPath, "DEFAULT_ENABLE=false\n");
diff --git a/tests/handler_test.php b/tests/handler_test.php
index 0dabe27..11ae277 100644
--- a/tests/handler_test.php
+++ b/tests/handler_test.php
@@ -28,3 +28,51 @@ test('directadmin user hooks are clickable and point to reserved icon path', fun
assert_contains(' 'Urlop',
'body' => "Dzień dobry\r\nWracam jutro",
- 'start_value' => '2026-06-10T09:00',
- 'end_value' => '2026-06-24T17:00',
+ 'start_value' => '2026-06-10T09:37',
+ 'end_value' => '2026-06-24T17:12',
'enabled' => '1',
], $settings);
assert_same('Urlop', $rule['subject']);
assert_same("Dzień dobry\nWracam jutro", $rule['body']);
assert_same('exact_hour', $rule['schedule_mode']);
+ assert_same('2026-06-10T09:37', $rule['start_value']);
assert_true($rule['start_ts'] < $rule['end_ts']);
});
+test('rule validator accepts da period schedule when backend is da', function (): void {
+ $settingsPath = TEST_TMP . '/validator-da.conf';
+ test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=da\n");
+ $settings = Settings::load($settingsPath);
+ $rule = RuleValidator::validate([
+ 'subject' => 'Urlop',
+ 'body' => 'Body',
+ 'start_value' => '2026-06-10|morning',
+ 'end_value' => '2026-06-24|evening',
+ 'enabled' => '1',
+ ], $settings);
+ assert_same('directadmin_period', $rule['schedule_mode']);
+});
+
test('rule validator rejects header injection and reversed dates', function (): void {
$settings = Settings::load(TEST_TMP . '/missing.conf');
try {
|