fix: harden backend synchronization

This commit is contained in:
Marek Miklewicz
2026-06-02 21:06:25 +02:00
parent 4b531a4c24
commit 64b62a5793
20 changed files with 254 additions and 16 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ final class AppContext
echo '<header><h1>' . self::e($this->t($title)) . '</h1><p>' . self::e($this->t('Manage automatic replies for account mailboxes')) . '</p></header>';
echo '<div class="top-actions"><a class="btn amber' . ($this->action() === 'index' ? ' active' : '') . '" href="' . self::e($this->url('index.html')) . '">' . self::e($this->t('AUTORESPONDERS')) . '</a>';
echo '<a class="btn amber' . ($this->action() === 'create' ? ' active' : '') . '" href="' . self::e($this->url('create.html')) . '">' . self::e($this->t('ADD AUTORESPONDER')) . '</a></div>';
echo '<section class="main-section">' . $alerts . $this->lang->translateHtml($body) . '</section></div>';
echo '<section class="main-section">' . $alerts . $body . '</section></div>';
echo '<script>' . $this->scripts() . '</script></body></html>';
}
+37 -4
View File
@@ -5,9 +5,11 @@ class DirectAdminVacationApi
{
/** @var callable|null */
private $request;
private string $daBinary = '/usr/local/bin/da';
public function __construct(private string $daBinary = '/usr/local/bin/da', ?callable $request = null)
public function __construct(string $daBinary = '/usr/local/bin/da', ?callable $request = null)
{
$this->daBinary = $daBinary;
$this->request = $request;
}
@@ -47,6 +49,37 @@ class DirectAdminVacationApi
$this->requestAsUser($owner, 'CMD_API_EMAIL_VACATION', $this->buildSavePayload($email, $rule, $exists));
}
public function vacationExists(string $owner, string $email): bool
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid mailbox email');
}
[$local, $domain] = explode('@', strtolower($email), 2);
return in_array($local, $this->listVacations($owner, $domain), true);
}
/** @return string[] */
public function listVacations(string $owner, string $domain): array
{
if (!preg_match('/^[A-Za-z0-9.-]+$/', $domain)) {
throw new InvalidArgumentException('Invalid domain');
}
$response = $this->requestAsUser($owner, 'CMD_API_EMAIL_VACATION', ['domain' => strtolower($domain)]);
$users = [];
foreach (explode('&', trim($response)) as $pair) {
if ($pair === '' || !str_contains($pair, '=')) {
continue;
}
[$key] = explode('=', $pair, 2);
$local = strtolower(urldecode($key));
if (preg_match('/^[A-Za-z0-9._+-]+$/', $local)) {
$users[] = $local;
}
}
sort($users);
return array_values(array_unique($users));
}
public function deleteVacation(string $owner, string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
@@ -70,7 +103,7 @@ class DirectAdminVacationApi
}
/** @param array<string,string> $payload */
private function requestAsUser(string $owner, string $endpoint, array $payload): void
private function requestAsUser(string $owner, string $endpoint, array $payload): string
{
if (!preg_match('/^[A-Za-z0-9._-]+$/', $owner)) {
throw new InvalidArgumentException('Invalid DirectAdmin owner');
@@ -80,7 +113,7 @@ class DirectAdminVacationApi
if ($result === false) {
throw new RuntimeException('DirectAdmin API request failed');
}
return;
return (string)$result;
}
$baseUrl = $this->apiUrl($owner);
$url = rtrim($baseUrl, '/') . '/' . $endpoint;
@@ -90,7 +123,7 @@ class DirectAdminVacationApi
$args[] = $key . '=' . $value;
}
$args[] = $url;
$this->run($args, '');
return $this->run($args, '');
}
private function apiUrl(string $owner): string
+5 -1
View File
@@ -49,7 +49,11 @@ final class DirectAdminVacationSyncService
$next = [];
foreach ($targets as $mailbox) {
$email = strtolower((string)$mailbox['email']);
$this->api->saveVacation($username, $email, $rule, isset($existing[$email]));
$isTracked = isset($existing[$email]);
if (!$isTracked && $this->api->vacationExists($username, $email)) {
throw new RuntimeException('Untracked DirectAdmin vacation already exists for ' . $email);
}
$this->api->saveVacation($username, $email, $rule, $isTracked);
$next[] = [
'rule_id' => $ruleId,
'domain' => (string)$mailbox['domain'],