From f3abc7b33f7c69d0424aa27de026901489aa7af0 Mon Sep 17 00:00:00 2001 From: Marek Miklewicz Date: Tue, 2 Jun 2026 19:55:07 +0200 Subject: [PATCH] feat: derive autoresponder schedule from backend --- exec/bootstrap.php | 2 ++ exec/handlers/_form_helpers.php | 43 ++++++++++++++-------- exec/handlers/delete.php | 4 +-- exec/handlers/index.php | 6 ++-- exec/handlers/preview.php | 8 ++--- exec/handlers/toggle.php | 2 +- exec/handlers/update.php | 2 +- exec/lib/AppContext.php | 6 ++-- exec/lib/BackendRuntimeConfig.php | 51 +++++++++++++++++++++++++++ exec/lib/RuleValidator.php | 2 +- exec/lib/Settings.php | 13 ++++--- lang/en.php | 17 +++++++++ lang/pl.php | 17 +++++++++ plugin-settings.conf | 8 ++--- plugin.conf | 2 +- tests/backend_runtime_config_test.php | 21 +++++++++++ tests/bootstrap_test.php | 18 ++++++++-- tests/handler_test.php | 48 +++++++++++++++++++++++++ tests/rule_validator_test.php | 21 +++++++++-- 19 files changed, 248 insertions(+), 43 deletions(-) create mode 100644 exec/lib/BackendRuntimeConfig.php create mode 100644 tests/backend_runtime_config_test.php diff --git a/exec/bootstrap.php b/exec/bootstrap.php index c88fb1a..f4ea95b 100644 --- a/exec/bootstrap.php +++ b/exec/bootstrap.php @@ -12,6 +12,7 @@ define('PLUGIN_EXEC_DIR', __DIR__); foreach ([ 'Settings.php', + 'BackendRuntimeConfig.php', 'Http.php', 'DirectAdminUser.php', 'Lang.php', @@ -30,6 +31,7 @@ require_once PLUGIN_EXEC_DIR . '/handlers/_form_helpers.php'; try { $settings = Settings::load(Settings::EXTERNAL_SETTINGS); + BackendRuntimeConfig::sync($settings, PLUGIN_ROOT . '/scripts/backend.sh', Settings::CONFIG_DIR . '/backend-state.json'); $user = DirectAdminUser::fromEnvironment($settings); $lang = Lang::load($user->language(), $settings); if (!$user->hasPluginAccess($settings)) { diff --git a/exec/handlers/_form_helpers.php b/exec/handlers/_form_helpers.php index 5a69f38..8b1fadd 100644 --- a/exec/handlers/_form_helpers.php +++ b/exec/handlers/_form_helpers.php @@ -16,14 +16,12 @@ function render_rule_form(AppContext $ctx, string $mode, ?array $rule): string $hiddenId = $isUpdate ? '' : ''; return '
' . $csrf . $hiddenId . - '

Treść odpowiedzi

' . + '

' . AppContext::e($ctx->t('Response content')) . '

' . '
' . '
' . - '

Podsumowanie

Zakres' . AppContext::e($ctx->t('All valid POP/IMAP mailboxes in this account')) . '
' . - '
Start' . AppContext::e($startValue) . '
Koniec' . AppContext::e($endValue) . '
' . - '
' . AppContext::e($ctx->t('Cancel')) . '
' . - render_calendar_picker('start_value', 'Data rozpoczęcia', $startValue) . - render_calendar_picker('end_value', 'Data zakończenia', $endValue) . + render_calendar_picker($ctx, 'start_value', $ctx->t('Start date'), $startValue) . + render_calendar_picker($ctx, 'end_value', $ctx->t('End date'), $endValue) . + '
' . AppContext::e($ctx->t('Cancel')) . '
' . ''; } @@ -32,18 +30,28 @@ function normalize_rule_post(array $post): array { foreach (['start_value', 'end_value'] as $key) { $date = trim((string)($post[$key . '_date'] ?? '')); - $hour = trim((string)($post[$key . '_hour'] ?? '')); - if ($date !== '' && preg_match('/^[0-9]{2}$/', $hour)) { - $post[$key] = $date . 'T' . $hour . ':00'; + $time = trim((string)($post[$key . '_time'] ?? '')); + $period = trim((string)($post[$key . '_period'] ?? '')); + if ($date !== '' && preg_match('/^[0-9]{2}:[0-9]{2}$/', $time)) { + $post[$key] = $date . 'T' . $time; + } elseif ($date !== '' && preg_match('/^(morning|afternoon|evening)$/', $period)) { + $post[$key] = $date . '|' . $period; } } return $post; } -function render_calendar_picker(string $name, string $title, string $value): string +function render_calendar_picker(AppContext $ctx, string $name, string $title, string $value): string { + $mode = $ctx->settings->scheduleMode(); $selectedDate = substr($value, 0, 10); - $selectedHour = substr($value, 11, 2) ?: '09'; + $selectedTime = '09:00'; + $selectedPeriod = 'morning'; + if ($mode === 'exact_hour' && preg_match('/T([0-9]{2}:[0-9]{2})$/', $value, $m)) { + $selectedTime = $m[1]; + } elseif ($mode === 'directadmin_period' && preg_match('/\|(morning|afternoon|evening)$/', $value, $m)) { + $selectedPeriod = $m[1]; + } $days = ''; for ($week = 0, $day = 1; $week < 4; $week++) { $days .= ''; @@ -54,14 +62,19 @@ function render_calendar_picker(string $name, string $title, string $value): str } $days .= ''; } - $hours = ''; - foreach (['08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18'] as $hour) { - $hours .= ''; + if ($mode === 'exact_hour') { + $timeControl = ''; + } else { + $timeControl = ''; } return '

' . AppContext::e($title) . '

' . AppContext::e($title) . '

Czerwiec 2026
' . '' . $days . '
PnWtŚrCzPtSbNd
' . '
Wybrano: ' . AppContext::e($selectedDate) . '
' . - '

Godzina

Wybierz godzinę
' . $hours . '
' . + '

' . AppContext::e($ctx->t('Time')) . '

' . $timeControl . '
' . '
'; } diff --git a/exec/handlers/delete.php b/exec/handlers/delete.php index d052cb5..a50ed1e 100644 --- a/exec/handlers/delete.php +++ b/exec/handlers/delete.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('Confirm deletion', '
Rule not found
'); + $ctx->render('Confirm deletion', '
' . AppContext::e($ctx->t('Rule not found')) . '
'); return; } if (Http::isPost()) { @@ -20,7 +20,7 @@ return static function (AppContext $ctx): void { } } $body = '

' . AppContext::e($ctx->t('Preview autoresponder')) . '

' . render_preview_box($rule) . '
' . - '

' . AppContext::e($ctx->t('Confirm deletion')) . '

Usunięcie reguły wyłączy odpowiedź automatyczną utworzoną przez tę regułę dla objętych skrzynek.
' . + '

' . AppContext::e($ctx->t('Confirm deletion')) . '

' . AppContext::e($ctx->t('Deleting this rule will disable the automatic reply created by this rule for covered mailboxes.')) . '
' . '
' . $ctx->csrfField('delete:' . $id) . '' . '
' . AppContext::e($ctx->t('Do not delete')) . '
'; $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 .= '
' . $ctx->csrfField('toggle:' . $id) . '
'; @@ -35,6 +35,6 @@ return static function (AppContext $ctx): void { $body = '

' . AppContext::e($ctx->t('Global autoresponders')) . '

' . AppContext::e($ctx->t('All valid POP/IMAP mailboxes in this account')) . '

' . '' . AppContext::e($ctx->t('Add autoresponder')) . '
' . - '' . $rows . '
Status' . AppContext::e($ctx->t('Subject')) . 'Okres wysyłkiZakresAkcje
'; + '' . $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')) . '
'; $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ły

Status' . 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 {