274 lines
10 KiB
PHP
274 lines
10 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
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, ?callable $runner = null)
|
|
{
|
|
$this->daBinary = $daBinary;
|
|
$this->request = $request;
|
|
$this->runner = $runner;
|
|
}
|
|
|
|
public function directAdminBinary(): string
|
|
{
|
|
return $this->daBinary;
|
|
}
|
|
|
|
/** @param array<string,mixed> $rule @return array<string,string> */
|
|
public function buildSavePayload(string $email, array $rule, bool $exists = true): array
|
|
{
|
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
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'] ?? ''));
|
|
[$startYear, $startMonth, $startDay] = explode('-', $startDate);
|
|
[$endYear, $endMonth, $endDay] = explode('-', $endDate);
|
|
$subjectPrefix = (string)($rule['subject_prefix'] ?? $rule['subject'] ?? '');
|
|
$payload = [
|
|
'action' => $exists ? 'modify' : 'create',
|
|
'domain' => $domain,
|
|
'user' => $local,
|
|
'text' => (string)($rule['body'] ?? ''),
|
|
'subject' => $subjectPrefix,
|
|
'reply_encoding' => 'UTF-8',
|
|
'reply_content_type' => 'text/plain',
|
|
'reply_once_time' => $this->replyOnceTime($rule),
|
|
'starttime' => $startTime,
|
|
'startyear' => $startYear,
|
|
'startmonth' => $startMonth,
|
|
'startday' => $startDay,
|
|
'endtime' => $endTime,
|
|
'endyear' => $endYear,
|
|
'endmonth' => $endMonth,
|
|
'endday' => $endDay,
|
|
];
|
|
if (!$exists) {
|
|
$payload['create'] = 'Create';
|
|
}
|
|
return $payload;
|
|
}
|
|
|
|
/** @param array<string,mixed> $rule */
|
|
public function saveVacation(string $owner, string $email, array $rule, bool $exists): void
|
|
{
|
|
$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)) {
|
|
throw new InvalidArgumentException('Invalid mailbox email');
|
|
}
|
|
[$local, $domain] = explode('@', strtolower($email), 2);
|
|
$this->requestAsUser($owner, 'CMD_API_EMAIL_VACATION', [
|
|
'action' => 'delete',
|
|
'domain' => $domain,
|
|
'select0' => $local,
|
|
]);
|
|
}
|
|
|
|
/** @return array{0:string,1:string} */
|
|
private function splitPeriod(string $value): 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');
|
|
}
|
|
return [$m[1], $m[2]];
|
|
}
|
|
|
|
/** @param array<string,string> $payload */
|
|
private function requestAsUser(string $owner, string $endpoint, array $payload): string
|
|
{
|
|
if (!preg_match('/^[A-Za-z0-9._-]+$/', $owner)) {
|
|
throw new InvalidArgumentException('Invalid DirectAdmin owner');
|
|
}
|
|
if ($this->request !== null) {
|
|
$result = call_user_func($this->request, $owner, $endpoint, $payload);
|
|
if ($result === false) {
|
|
throw new RuntimeException('DirectAdmin API request failed');
|
|
}
|
|
return (string)$result;
|
|
}
|
|
$baseUrl = $this->apiUrl($owner);
|
|
$url = rtrim($baseUrl, '/') . '/' . $endpoint;
|
|
$args = ['curl', '-sS', '-X', 'POST', '-w', "\n%{http_code}"];
|
|
foreach ($payload as $key => $value) {
|
|
$args[] = '--data-urlencode';
|
|
$args[] = $key . '=' . $value;
|
|
}
|
|
$args[] = $url;
|
|
return $this->parseHttpResponse($this->run($args, ''));
|
|
}
|
|
|
|
/** @param array<string,mixed> $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
|
|
{
|
|
$output = $this->run([$this->resolveDirectAdminBinary(), 'api-url', '--user=' . $owner], '');
|
|
$url = trim($output);
|
|
if ($url === '' || !preg_match('#^https?://#', $url)) {
|
|
throw new RuntimeException('Cannot determine DirectAdmin API URL for user');
|
|
}
|
|
return $url;
|
|
}
|
|
|
|
private function resolveDirectAdminBinary(): string
|
|
{
|
|
$candidates = array_values(array_unique([
|
|
$this->daBinary,
|
|
'/usr/local/directadmin/directadmin',
|
|
'/usr/bin/da',
|
|
]));
|
|
foreach ($candidates as $candidate) {
|
|
if ($candidate !== '' && is_executable($candidate)) {
|
|
return $candidate;
|
|
}
|
|
}
|
|
throw new RuntimeException('DirectAdmin binary is not executable. Checked: ' . implode(', ', $candidates));
|
|
}
|
|
|
|
/** @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]);
|
|
}
|
|
$process = proc_open($args, [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes);
|
|
if (!is_resource($process)) {
|
|
throw new RuntimeException('Cannot start DirectAdmin API command');
|
|
}
|
|
fwrite($pipes[0], $stdin);
|
|
fclose($pipes[0]);
|
|
$stdout = stream_get_contents($pipes[1]) ?: '';
|
|
$stderr = stream_get_contents($pipes[2]) ?: '';
|
|
fclose($pipes[1]);
|
|
fclose($pipes[2]);
|
|
$code = proc_close($process);
|
|
if ($code !== 0) {
|
|
throw new RuntimeException('DirectAdmin API command failed: ' . trim($stderr));
|
|
}
|
|
return $stdout;
|
|
}
|
|
}
|