From 27d95e1baf2d7442e8057764d8891b7548a0c42e Mon Sep 17 00:00:00 2001 From: Marek Miklewicz Date: Tue, 2 Jun 2026 23:10:01 +0200 Subject: [PATCH] fix: align directadmin vacation api payload --- exec/lib/DirectAdminVacationApi.php | 107 +++++++++++++++++++++--- plugin.conf | 2 +- tests/directadmin_vacation_api_test.php | 62 +++++++++++++- 3 files changed, 158 insertions(+), 13 deletions(-) diff --git a/exec/lib/DirectAdminVacationApi.php b/exec/lib/DirectAdminVacationApi.php index b9af4d0..c977586 100644 --- a/exec/lib/DirectAdminVacationApi.php +++ b/exec/lib/DirectAdminVacationApi.php @@ -5,12 +5,15 @@ class DirectAdminVacationApi { /** @var callable|null */ private $request; + /** @var callable|null */ + private $runner; private string $daBinary = '/usr/local/directadmin/directadmin'; - public function __construct(string $daBinary = '/usr/local/directadmin/directadmin', ?callable $request = null) + public function __construct(string $daBinary = '/usr/local/directadmin/directadmin', ?callable $request = null, ?callable $runner = null) { $this->daBinary = $daBinary; $this->request = $request; + $this->runner = $runner; } public function directAdminBinary(): string @@ -30,17 +33,15 @@ class DirectAdminVacationApi [$startYear, $startMonth, $startDay] = explode('-', $startDate); [$endYear, $endMonth, $endDay] = explode('-', $endDate); $subjectPrefix = (string)($rule['subject_prefix'] ?? $rule['subject'] ?? ''); - $replyFrequency = !empty($rule['reply_every_message']) ? '0' : (string)max(0, (int)($rule['repeat_minutes'] ?? 1440)); - return [ + $payload = [ 'action' => $exists ? 'modify' : 'create', 'domain' => $domain, 'user' => $local, - 'email' => $email, 'text' => (string)($rule['body'] ?? ''), - 'subject_prefix' => $subjectPrefix, 'subject' => $subjectPrefix, - 'reply_frequency' => $replyFrequency, - 'reply_once_time' => $replyFrequency, + 'reply_encoding' => 'UTF-8', + 'reply_content_type' => 'text/plain', + 'reply_once_time' => $this->replyOnceTime($rule), 'starttime' => $startTime, 'startyear' => $startYear, 'startmonth' => $startMonth, @@ -49,8 +50,11 @@ class DirectAdminVacationApi 'endyear' => $endYear, 'endmonth' => $endMonth, 'endday' => $endDay, - 'create' => $exists ? '' : 'Create', ]; + if (!$exists) { + $payload['create'] = 'Create'; + } + return $payload; } /** @param array $rule */ @@ -127,13 +131,92 @@ class DirectAdminVacationApi } $baseUrl = $this->apiUrl($owner); $url = rtrim($baseUrl, '/') . '/' . $endpoint; - $args = ['curl', '-fsS', '-X', 'POST']; + $args = ['curl', '-sS', '-X', 'POST', '-w', "\n%{http_code}"]; foreach ($payload as $key => $value) { $args[] = '--data-urlencode'; $args[] = $key . '=' . $value; } $args[] = $url; - return $this->run($args, ''); + return $this->parseHttpResponse($this->run($args, '')); + } + + /** @param array $rule */ + private function replyOnceTime(array $rule): string + { + if (!empty($rule['reply_every_message'])) { + return '0'; + } + return match ((int)($rule['repeat_minutes'] ?? 1440)) { + 30 => '30m', + 60 => '1h', + 120 => '2h', + 360 => '6h', + 720 => '12h', + 1440 => '1d', + 2880 => '2d', + 10080 => '1w', + default => '1d', + }; + } + + private function parseHttpResponse(string $output): string + { + if (!preg_match("/\n([0-9]{3})$/", $output, $m, PREG_OFFSET_CAPTURE)) { + return $output; + } + $code = (int)$m[1][0]; + $body = substr($output, 0, (int)$m[0][1]); + if ($code < 200 || $code >= 400) { + throw new RuntimeException('DirectAdmin API HTTP ' . $code . ': ' . $this->compactBody($body)); + } + $this->throwIfApiError($body); + return $body; + } + + private function throwIfApiError(string $body): void + { + $trimmed = trim($body); + if ($trimmed === '') { + return; + } + + $json = json_decode($trimmed, true); + if (is_array($json) && isset($json['error']) && $this->truthyError($json['error'])) { + throw new RuntimeException('DirectAdmin API error: ' . $this->compactBody(implode(' ', array_filter([ + (string)($json['text'] ?? ''), + (string)($json['details'] ?? ''), + (string)($json['result'] ?? ''), + (string)($json['error'] ?? ''), + ])))); + } + + $fields = []; + parse_str($trimmed, $fields); + if (isset($fields['error']) && $this->truthyError($fields['error'])) { + throw new RuntimeException('DirectAdmin API error: ' . $this->compactBody(implode(' ', array_filter([ + is_scalar($fields['text'] ?? null) ? (string)$fields['text'] : '', + is_scalar($fields['details'] ?? null) ? (string)$fields['details'] : '', + is_scalar($fields['result'] ?? null) ? (string)$fields['result'] : '', + is_scalar($fields['error'] ?? null) ? (string)$fields['error'] : '', + ])))); + } + } + + private function truthyError(mixed $value): bool + { + return in_array(strtolower(trim((string)$value)), ['1', 'yes', 'true', 'error'], true); + } + + private function compactBody(string $body): string + { + $body = trim(preg_replace('/[[:cntrl:]]+/', ' ', $body) ?? $body); + if ($body === '') { + return 'empty response body'; + } + if (strlen($body) > 1200) { + return substr($body, 0, 1200) . '...'; + } + return $body; } private function apiUrl(string $owner): string @@ -164,6 +247,10 @@ class DirectAdminVacationApi /** @param string[] $args */ private function run(array $args, string $stdin): string { + if ($this->runner !== null) { + $result = call_user_func($this->runner, $args, $stdin); + return (string)$result; + } if (($args[0] ?? '') !== '' && str_contains($args[0], '/') && !is_executable($args[0])) { throw new RuntimeException('DirectAdmin API command is not executable: ' . $args[0]); } diff --git a/plugin.conf b/plugin.conf index d6b47db..6402ff9 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.2 +version=1.1.3 active=no installed=no user_run_as=root diff --git a/tests/directadmin_vacation_api_test.php b/tests/directadmin_vacation_api_test.php index 6ec980d..567ca57 100644 --- a/tests/directadmin_vacation_api_test.php +++ b/tests/directadmin_vacation_api_test.php @@ -22,12 +22,70 @@ test('directadmin vacation api builds scoped payload for one mailbox', function assert_same('info', $payload['user']); assert_same('example.com', $payload['domain']); assert_same('Body', $payload['text']); - assert_same('Urlop', $payload['subject_prefix']); - assert_same('1440', $payload['reply_frequency']); + assert_same('Urlop', $payload['subject']); + assert_same('1d', $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']); }); +test('directadmin vacation api reports http error response bodies', function (): void { + $runner = static function (array $args, string $stdin): string { + if ($args[0] === '/bin/echo') { + return 'https://directadmin.invalid/CMD_LOGIN_KEY'; + } + assert_same('curl', $args[0]); + return "error=1&text=Unable to set vacation&details=Invalid repeat value\n500"; + }; + + $api = new DirectAdminVacationApi('/bin/echo', null, $runner); + try { + $api->saveVacation('alice', 'info@example.com', [ + 'subject_prefix' => 'Urlop', + 'reply_every_message' => false, + 'repeat_minutes' => 1440, + 'body' => 'Body', + 'start_value' => '2026-06-10|morning', + 'end_value' => '2026-06-24|evening', + ], false); + } catch (RuntimeException $e) { + assert_contains('DirectAdmin API HTTP 500', $e->getMessage()); + assert_contains('Invalid repeat value', $e->getMessage()); + return; + } + + throw new RuntimeException('Expected HTTP error to fail'); +}); + +test('directadmin vacation api reports legacy api error payloads', function (): void { + $runner = static function (array $args, string $stdin): string { + if ($args[0] === '/bin/echo') { + return 'https://directadmin.invalid/CMD_LOGIN_KEY'; + } + return "error=1&text=Unable%20to%20set%20vacation&details=Bad%20mailbox\n200"; + }; + + $api = new DirectAdminVacationApi('/bin/echo', null, $runner); + try { + $api->saveVacation('alice', 'info@example.com', [ + 'subject_prefix' => 'Urlop', + 'reply_every_message' => false, + 'repeat_minutes' => 1440, + 'body' => 'Body', + 'start_value' => '2026-06-10|morning', + 'end_value' => '2026-06-24|evening', + ], false); + } catch (RuntimeException $e) { + assert_contains('DirectAdmin API error', $e->getMessage()); + assert_contains('Bad mailbox', $e->getMessage()); + return; + } + + throw new RuntimeException('Expected legacy API error to fail'); +}); + test('directadmin vacation api detects existing native vacation by mailbox', function (): void { $api = new DirectAdminVacationApi('/usr/local/directadmin/directadmin', static function (string $owner, string $endpoint, array $payload): string { assert_same('alice', $owner);