diff --git a/exec/handlers/_form_helpers.php b/exec/handlers/_form_helpers.php index b77172a..8921ce2 100644 --- a/exec/handlers/_form_helpers.php +++ b/exec/handlers/_form_helpers.php @@ -6,7 +6,7 @@ function render_rule_form(AppContext $ctx, string $mode, ?array $rule): string $isUpdate = $mode === 'update'; $id = $isUpdate ? (string)$rule['id'] : ''; $subjectPrefix = $rule['subject_prefix'] ?? $rule['subject'] ?? ''; - $replyFrequency = selected_reply_frequency($rule); + $replyFrequency = selected_reply_frequency($ctx, $rule); $body = $rule['body'] ?? ''; $enabled = $rule === null || !empty($rule['enabled']); $startValue = (string)($rule['start_value'] ?? default_schedule_value($ctx, true)); @@ -30,29 +30,43 @@ function render_rule_form(AppContext $ctx, string $mode, ?array $rule): string function default_schedule_value(AppContext $ctx, bool $start): string { $today = new DateTimeImmutable('today', new DateTimeZone($ctx->settings->timezone())); - if ($ctx->settings->scheduleMode() === 'directadmin_period') { - return $today->format('Y-m-d') . '|' . ($start ? 'morning' : 'evening'); - } - return $today->format('Y-m-d') . 'T' . ($start ? '09:00' : '17:00'); + return $today->format('Y-m-d') . 'T' . ($start ? '00:00' : '23:59'); } /** @param array|null $rule */ -function selected_reply_frequency(?array $rule): string +function selected_reply_frequency(AppContext $ctx, ?array $rule): string { if ($rule === null) { return '1440'; } if (!empty($rule['reply_every_message'])) { - return 'every'; + return $ctx->settings->backendMode() === 'da' ? '1' : 'every'; } $minutes = (string)(int)($rule['repeat_minutes'] ?? 1440); - return in_array($minutes, ['30', '60', '120', '360', '720', '1440', '2880', '10080'], true) ? $minutes : '1440'; + return array_key_exists($minutes, reply_frequency_options($ctx)) ? $minutes : '1440'; } function render_reply_frequency_select(AppContext $ctx, string $selected): string { - $options = [ - 'every' => 'Send automatic reply for every message', + $options = reply_frequency_options($ctx); + $html = ''; +} + +/** @return array */ +function reply_frequency_options(AppContext $ctx): array +{ + $options = []; + if ($ctx->settings->backendMode() !== 'da') { + $options['every'] = 'Send automatic reply for every message'; + } + return $options + [ + '1' => '1 minute', + '10' => '10 minutes', '30' => '30 minutes', '60' => '1 hour', '120' => '2 hours', @@ -60,14 +74,19 @@ function render_reply_frequency_select(AppContext $ctx, string $selected): strin '720' => '12 hours', '1440' => '1 day', '2880' => '2 days', + '4320' => '3 days', + '5760' => '4 days', + '7200' => '5 days', + '8640' => '6 days', '10080' => '7 days', + '11520' => '8 days', + '12960' => '9 days', + '14400' => '10 days', + '15840' => '11 days', + '17280' => '12 days', + '18720' => '13 days', + '20160' => '14 days', ]; - $html = ''; } /** @param array $post @return array */ diff --git a/exec/handlers/index.php b/exec/handlers/index.php index 35bf9af..14f15a7 100644 --- a/exec/handlers/index.php +++ b/exec/handlers/index.php @@ -26,7 +26,7 @@ return static function (AppContext $ctx): void { $rows .= '' . AppContext::e($ctx->t($enabled ? 'Enabled' : 'Disabled')) . ''; $rows .= '' . AppContext::e($subjectPrefix) . '
' . date('Y-m-d H:i', (int)($rule['updated_at'] ?? time())) . ''; $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 .= ''; $rows .= '' . AppContext::e($ctx->t('Edit')) . ''; $rows .= '' . AppContext::e($ctx->t('Preview')) . ''; $rows .= '
' . $ctx->csrfField('toggle:' . $id) . '
'; @@ -36,6 +36,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 . '
' . AppContext::e($ctx->t('Status')) . '' . AppContext::e($ctx->t('Subject prefix')) . '' . AppContext::e($ctx->t('Sending period')) . '' . AppContext::e($ctx->t('Scope')) . '' . AppContext::e($ctx->t('Actions')) . '
'; + '' . $rows . '
' . AppContext::e($ctx->t('Status')) . '' . AppContext::e($ctx->t('Subject prefix')) . '' . AppContext::e($ctx->t('Sending period')) . '' . AppContext::e($ctx->t('Actions')) . '
'; $ctx->render('Global autoresponders', $body, $ok); }; diff --git a/exec/lib/DirectAdminVacationApi.php b/exec/lib/DirectAdminVacationApi.php index c977586..2d582b4 100644 --- a/exec/lib/DirectAdminVacationApi.php +++ b/exec/lib/DirectAdminVacationApi.php @@ -28,8 +28,8 @@ class DirectAdminVacationApi throw new InvalidArgumentException('Invalid mailbox email'); } [$local, $domain] = explode('@', strtolower($email), 2); - [$startDate, $startTime] = $this->splitPeriod((string)($rule['start_value'] ?? '')); - [$endDate, $endTime] = $this->splitPeriod((string)($rule['end_value'] ?? '')); + [$startDate, $startTime] = $this->splitScheduleValue((string)($rule['start_value'] ?? ''), true); + [$endDate, $endTime] = $this->splitScheduleValue((string)($rule['end_value'] ?? ''), false); [$startYear, $startMonth, $startDay] = explode('-', $startDate); [$endYear, $endMonth, $endDay] = explode('-', $endDate); $subjectPrefix = (string)($rule['subject_prefix'] ?? $rule['subject'] ?? ''); @@ -108,12 +108,16 @@ class DirectAdminVacationApi } /** @return array{0:string,1:string} */ - private function splitPeriod(string $value): array + private function splitScheduleValue(string $value, bool $start): array { - if (!preg_match('/^([0-9]{4}-[0-9]{2}-[0-9]{2})\|(morning|afternoon|evening)$/', $value, $m)) { - throw new InvalidArgumentException('DirectAdmin API backend requires directadmin_period values'); + if (preg_match('/^([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2})$/', $value, $m)) { + return [$m[1], $m[2]]; } - return [$m[1], $m[2]]; + if (!preg_match('/^([0-9]{4}-[0-9]{2}-[0-9]{2})\|(morning|afternoon|evening)$/', $value, $m)) { + throw new InvalidArgumentException('DirectAdmin API backend requires exact date-time values'); + } + $time = ['morning' => '08:00', 'afternoon' => '13:00', 'evening' => $start ? '18:00' : '23:59'][$m[2]]; + return [$m[1], $time]; } /** @param array $payload */ @@ -144,9 +148,11 @@ class DirectAdminVacationApi private function replyOnceTime(array $rule): string { if (!empty($rule['reply_every_message'])) { - return '0'; + return '1m'; } return match ((int)($rule['repeat_minutes'] ?? 1440)) { + 1 => '1m', + 10 => '10m', 30 => '30m', 60 => '1h', 120 => '2h', @@ -154,7 +160,18 @@ class DirectAdminVacationApi 720 => '12h', 1440 => '1d', 2880 => '2d', + 4320 => '3d', + 5760 => '4d', + 7200 => '5d', + 8640 => '6d', 10080 => '1w', + 11520 => '8d', + 12960 => '9d', + 14400 => '10d', + 15840 => '11d', + 17280 => '12d', + 18720 => '13d', + 20160 => '14d', default => '1d', }; } diff --git a/exec/lib/RuleValidator.php b/exec/lib/RuleValidator.php index 9dee7d9..53c9fd3 100644 --- a/exec/lib/RuleValidator.php +++ b/exec/lib/RuleValidator.php @@ -5,6 +5,8 @@ final class RuleValidator { /** @var array */ private const REPLY_FREQUENCIES = [ + '1' => 1, + '10' => 10, '30' => 30, '60' => 60, '120' => 120, @@ -12,7 +14,18 @@ final class RuleValidator '720' => 720, '1440' => 1440, '2880' => 2880, + '4320' => 4320, + '5760' => 5760, + '7200' => 7200, + '8640' => 8640, '10080' => 10080, + '11520' => 11520, + '12960' => 12960, + '14400' => 14400, + '15840' => 15840, + '17280' => 17280, + '18720' => 18720, + '20160' => 20160, ]; /** @param array $input @return array */ @@ -45,7 +58,7 @@ final class RuleValidator if ($endTs <= $startTs) { throw new InvalidArgumentException('Data zakończenia musi być późniejsza niż data rozpoczęcia.'); } - [$replyEveryMessage, $repeatMinutes] = self::parseReplyFrequency((string)($input['reply_frequency'] ?? '1440')); + [$replyEveryMessage, $repeatMinutes] = self::parseReplyFrequency((string)($input['reply_frequency'] ?? '1440'), $settings); return [ 'subject_prefix' => $subjectPrefix, @@ -64,10 +77,13 @@ final class RuleValidator } /** @return array{0:bool,1:int} */ - private static function parseReplyFrequency(string $value): array + private static function parseReplyFrequency(string $value, Settings $settings): array { $value = trim(strtolower($value)); if ($value === 'every') { + if ($settings->backendMode() === 'da') { + throw new InvalidArgumentException('Częstość odpowiedzi jest nieprawidłowa.'); + } return [true, 0]; } if (isset(self::REPLY_FREQUENCIES[$value])) { diff --git a/exec/lib/Settings.php b/exec/lib/Settings.php index ac8adc9..25465f6 100644 --- a/exec/lib/Settings.php +++ b/exec/lib/Settings.php @@ -98,7 +98,7 @@ final class Settings public function scheduleMode(): string { - return $this->backendMode() === 'da' ? 'directadmin_period' : 'exact_hour'; + return 'exact_hour'; } public function dynamicMailboxScope(): bool diff --git a/lang/en.php b/lang/en.php index dc7418a..1c4a559 100644 --- a/lang/en.php +++ b/lang/en.php @@ -17,6 +17,8 @@ return [ 'Response content' => 'Response content', 'Response frequency' => 'Response frequency', 'Send automatic reply for every message' => 'Send automatic reply for every message', + '1 minute' => '1 minute', + '10 minutes' => '10 minutes', '30 minutes' => '30 minutes', '1 hour' => '1 hour', '2 hours' => '2 hours', @@ -24,7 +26,18 @@ return [ '12 hours' => '12 hours', '1 day' => '1 day', '2 days' => '2 days', + '3 days' => '3 days', + '4 days' => '4 days', + '5 days' => '5 days', + '6 days' => '6 days', '7 days' => '7 days', + '8 days' => '8 days', + '9 days' => '9 days', + '10 days' => '10 days', + '11 days' => '11 days', + '12 days' => '12 days', + '13 days' => '13 days', + '14 days' => '14 days', 'Status' => 'Status', 'Enabled' => 'Enabled', 'Disabled' => 'Disabled', diff --git a/lang/pl.php b/lang/pl.php index 70b23fe..1604860 100644 --- a/lang/pl.php +++ b/lang/pl.php @@ -17,6 +17,8 @@ return [ 'Response content' => 'Treść odpowiedzi', 'Response frequency' => 'Częstość odpowiedzi', 'Send automatic reply for every message' => 'Wysyłaj automatyczną odpowiedź dla każdej wiadomości', + '1 minute' => '1 minuta', + '10 minutes' => '10 minut', '30 minutes' => '30 minut', '1 hour' => '1 godzina', '2 hours' => '2 godziny', @@ -24,7 +26,18 @@ return [ '12 hours' => '12 godzin', '1 day' => '1 dzień', '2 days' => '2 dni', + '3 days' => '3 dni', + '4 days' => '4 dni', + '5 days' => '5 dni', + '6 days' => '6 dni', '7 days' => '7 dni', + '8 days' => '8 dni', + '9 days' => '9 dni', + '10 days' => '10 dni', + '11 days' => '11 dni', + '12 days' => '12 dni', + '13 days' => '13 dni', + '14 days' => '14 dni', 'Status' => 'Status', 'Enabled' => 'Włączony', 'Disabled' => 'Wyłączony', diff --git a/plugin.conf b/plugin.conf index bbfe99c..f8bd486 100644 --- a/plugin.conf +++ b/plugin.conf @@ -2,7 +2,7 @@ name=global-autoresponder id=global-autoresponder type=user author=HITME.PL -version=1.1.4 +version=1.1.5 active=no installed=no user_run_as=root diff --git a/tests/bootstrap_test.php b/tests/bootstrap_test.php index 152cc8f..f86fa6d 100644 --- a/tests/bootstrap_test.php +++ b/tests/bootstrap_test.php @@ -49,7 +49,7 @@ test('backend mode derives schedule mode', function (): void { test_write($path, "GLOBAL_AUTORESPONDER_BACKEND=da\n"); $settings = Settings::load($path); assert_same('da', $settings->backendMode()); - assert_same('directadmin_period', $settings->scheduleMode()); + assert_same('exact_hour', $settings->scheduleMode()); test_write($path, "GLOBAL_AUTORESPONDER_BACKEND=exim\n"); $settings = Settings::load($path); diff --git a/tests/directadmin_vacation_api_test.php b/tests/directadmin_vacation_api_test.php index 567ca57..4e14d2c 100644 --- a/tests/directadmin_vacation_api_test.php +++ b/tests/directadmin_vacation_api_test.php @@ -13,22 +13,36 @@ test('directadmin vacation api builds scoped payload for one mailbox', function $payload = $api->buildSavePayload('info@example.com', [ 'subject_prefix' => 'Urlop', 'reply_every_message' => false, - 'repeat_minutes' => 1440, + 'repeat_minutes' => 1, 'body' => 'Body', - 'start_value' => '2026-06-10|morning', - 'end_value' => '2026-06-24|evening', + 'start_value' => '2026-06-10T00:00', + 'end_value' => '2026-06-24T23:59', ]); assert_same('info', $payload['user']); assert_same('example.com', $payload['domain']); assert_same('Body', $payload['text']); assert_same('Urlop', $payload['subject']); - assert_same('1d', $payload['reply_once_time']); + assert_same('1m', $payload['reply_once_time']); assert_false(isset($payload['email'])); assert_false(isset($payload['subject_prefix'])); assert_false(isset($payload['reply_frequency'])); - assert_same('morning', $payload['starttime']); - assert_same('evening', $payload['endtime']); + assert_same('00:00', $payload['starttime']); + assert_same('23:59', $payload['endtime']); +}); + +test('directadmin vacation api maps every-message legacy rules to one minute minimum', function (): void { + $api = new DirectAdminVacationApi('/usr/local/directadmin/directadmin'); + $payload = $api->buildSavePayload('info@example.com', [ + 'subject_prefix' => 'Urlop', + 'reply_every_message' => true, + 'repeat_minutes' => 0, + 'body' => 'Body', + 'start_value' => '2026-06-10T00:00', + 'end_value' => '2026-06-24T23:59', + ]); + + assert_same('1m', $payload['reply_once_time']); }); test('directadmin vacation api reports http error response bodies', function (): void { diff --git a/tests/handler_test.php b/tests/handler_test.php index 94f97f9..4cea80f 100644 --- a/tests/handler_test.php +++ b/tests/handler_test.php @@ -59,11 +59,12 @@ test('rule form uses minute time input and no summary box in exim mode', functio assert_contains(': pierwotny temat', $html); assert_contains('Częstość odpowiedzi', $html); assert_contains('Wysyłaj automatyczną odpowiedź dla każdej wiadomości', $html); + assert_contains('14 dni', $html); assert_contains('1 dzień', $html); assert_contains('Treść wiadomości', $html); }); -test('rule form uses native period selector in da mode', function (): void { +test('rule form uses exact hour selector and directadmin frequency range in da mode', function (): void { $settingsPath = TEST_TMP . '/form-da.conf'; test_write($settingsPath, "DEFAULT_PLUGIN_LANGUAGE=pl\nGLOBAL_AUTORESPONDER_BACKEND=da\n"); $settings = Settings::load($settingsPath); @@ -76,9 +77,18 @@ test('rule form uses native period selector in da mode', function (): void { new AuditLog(TEST_TMP . '/audit.log') ); $html = render_rule_form($ctx, 'create', null); - assert_contains('name="start_value_period"', $html); - assert_contains('Rano', $html); - assert_false(strpos($html, 'type="time"') !== false); + assert_contains('type="time"', $html); + assert_contains('step="60"', $html); + assert_contains('1 minuta', $html); + assert_contains('14 dni', $html); + assert_false(strpos($html, 'Wysyłaj automatyczną odpowiedź dla każdej wiadomości') !== false); + assert_false(strpos($html, 'name="start_value_period"') !== false); +}); + +test('autoresponder table does not expose mailbox scope column', function (): void { + $source = file_get_contents(PLUGIN_ROOT . '/exec/handlers/index.php') ?: ''; + assert_false(strpos($source, "ctx->t('Scope')") !== false); + assert_false(strpos($source, "ctx->t(\$rule['dynamic_scope']") !== false); }); test('mutating handlers synchronize active backend after storage changes', function (): void { @@ -155,8 +165,8 @@ test('new rule form selects current date for start and end calendars by default' assert_contains('id="start_value_date" name="start_value_date" value="' . $today . '"', $html); assert_contains('id="end_value_date" name="end_value_date" value="' . $today . '"', $html); - assert_contains('id="start_value" name="start_value" value="' . $today . 'T09:00"', $html); - assert_contains('id="end_value" name="end_value" value="' . $today . 'T17:00"', $html); + assert_contains('id="start_value" name="start_value" value="' . $today . 'T00:00"', $html); + assert_contains('id="end_value" name="end_value" value="' . $today . 'T23:59"', $html); }); test('rule form localizes calendar and preview labels without translating user content globally', function (): void { @@ -194,8 +204,8 @@ test('directadmin backend allows only one enabled global rule per account', func 'repeat_minutes' => 1440, 'body' => 'Body', 'enabled' => true, - 'start_value' => '2026-06-10|morning', - 'end_value' => '2026-06-24|evening', + 'start_value' => '2026-06-10T00:00', + 'end_value' => '2026-06-24T23:59', ]); $ctx = new AppContext( new DirectAdminUser('alice', 'enhanced', 'pl', TEST_TMP . '/config'), diff --git a/tests/rule_validator_test.php b/tests/rule_validator_test.php index 590ef6f..986a0ad 100644 --- a/tests/rule_validator_test.php +++ b/tests/rule_validator_test.php @@ -26,21 +26,42 @@ test('rule validator normalizes exact hour schedule', function (): void { assert_true($rule['start_ts'] < $rule['end_ts']); }); -test('rule validator accepts da period schedule when backend is da', function (): void { +test('rule validator accepts da exact hour 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_prefix' => 'Urlop', - 'reply_frequency' => 'every', + 'reply_frequency' => '1', 'body' => 'Body', - 'start_value' => '2026-06-10|morning', - 'end_value' => '2026-06-24|evening', + 'start_value' => '2026-06-10T00:00', + 'end_value' => '2026-06-24T23:59', 'enabled' => '1', ], $settings); - assert_same('directadmin_period', $rule['schedule_mode']); - assert_true($rule['reply_every_message']); - assert_same(0, $rule['repeat_minutes']); + assert_same('exact_hour', $rule['schedule_mode']); + assert_false($rule['reply_every_message']); + assert_same(1, $rule['repeat_minutes']); + assert_same('2026-06-10T00:00', $rule['start_value']); + assert_same('2026-06-24T23:59', $rule['end_value']); +}); + +test('rule validator rejects every message frequency for da backend', function (): void { + $settingsPath = TEST_TMP . '/validator-da-every.conf'; + test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=da\n"); + $settings = Settings::load($settingsPath); + try { + RuleValidator::validate([ + 'subject_prefix' => 'Urlop', + 'reply_frequency' => 'every', + 'body' => 'Body', + 'start_value' => '2026-06-10T00:00', + 'end_value' => '2026-06-24T23:59', + ], $settings); + } catch (InvalidArgumentException $e) { + assert_contains('Częstość odpowiedzi', $e->getMessage()); + return; + } + throw new RuntimeException('Expected every-message frequency to fail for da backend'); }); test('rule validator rejects header injection and reversed dates', function (): void { @@ -66,8 +87,8 @@ test('rule validator rejects unsupported response frequency', function (): void 'subject_prefix' => 'Urlop', 'reply_frequency' => '45', 'body' => 'Body', - 'start_value' => '2026-06-10|morning', - 'end_value' => '2026-06-24|evening', + 'start_value' => '2026-06-10T00:00', + 'end_value' => '2026-06-24T23:59', ], $settings); } catch (InvalidArgumentException $e) { assert_contains('Częstość odpowiedzi', $e->getMessage());