feat: use vacation subject prefix
This commit is contained in:
@@ -5,7 +5,8 @@ function render_rule_form(AppContext $ctx, string $mode, ?array $rule): string
|
|||||||
{
|
{
|
||||||
$isUpdate = $mode === 'update';
|
$isUpdate = $mode === 'update';
|
||||||
$id = $isUpdate ? (string)$rule['id'] : '';
|
$id = $isUpdate ? (string)$rule['id'] : '';
|
||||||
$subject = $rule['subject'] ?? '';
|
$subjectPrefix = $rule['subject_prefix'] ?? $rule['subject'] ?? '';
|
||||||
|
$replyFrequency = selected_reply_frequency($rule);
|
||||||
$body = $rule['body'] ?? '';
|
$body = $rule['body'] ?? '';
|
||||||
$enabled = $rule === null || !empty($rule['enabled']);
|
$enabled = $rule === null || !empty($rule['enabled']);
|
||||||
$startValue = (string)($rule['start_value'] ?? '2026-06-10T09:00');
|
$startValue = (string)($rule['start_value'] ?? '2026-06-10T09:00');
|
||||||
@@ -16,7 +17,8 @@ function render_rule_form(AppContext $ctx, string $mode, ?array $rule): string
|
|||||||
$hiddenId = $isUpdate ? '<input type="hidden" name="id" value="' . AppContext::e($id) . '">' : '';
|
$hiddenId = $isUpdate ? '<input type="hidden" name="id" value="' . AppContext::e($id) . '">' : '';
|
||||||
|
|
||||||
return '<form method="post" action="' . AppContext::e($action) . '">' . $csrf . $hiddenId .
|
return '<form method="post" action="' . AppContext::e($action) . '">' . $csrf . $hiddenId .
|
||||||
'<div class="panel"><h3>' . AppContext::e($ctx->t('Response content')) . '</h3><div class="row"><div><label>' . AppContext::e($ctx->t('Subject')) . '</label><input type="text" name="subject" value="' . AppContext::e((string)$subject) . '"></div>' .
|
'<div class="panel"><h3>' . AppContext::e($ctx->t('Response content')) . '</h3><div class="row"><div><label>' . AppContext::e($ctx->t('Subject prefix')) . '</label><div class="subject-prefix-control"><input type="text" name="subject_prefix" value="' . AppContext::e((string)$subjectPrefix) . '"><span>' . AppContext::e($ctx->t('Original subject suffix')) . '</span></div></div>' .
|
||||||
|
'<div><label>' . AppContext::e($ctx->t('Response frequency')) . '</label>' . render_reply_frequency_select($ctx, $replyFrequency) . '</div>' .
|
||||||
'<div><label>' . AppContext::e($ctx->t('Status')) . '</label><label class="br-time-item"><input type="checkbox" name="enabled" value="1" ' . ($enabled ? 'checked' : '') . '> ' . AppContext::e($ctx->t('Enabled')) . '</label></div></div>' .
|
'<div><label>' . AppContext::e($ctx->t('Status')) . '</label><label class="br-time-item"><input type="checkbox" name="enabled" value="1" ' . ($enabled ? 'checked' : '') . '> ' . AppContext::e($ctx->t('Enabled')) . '</label></div></div>' .
|
||||||
'<label>' . AppContext::e($ctx->t('Message body')) . '</label><textarea name="body">' . AppContext::e((string)$body) . '</textarea></div>' .
|
'<label>' . AppContext::e($ctx->t('Message body')) . '</label><textarea name="body">' . AppContext::e((string)$body) . '</textarea></div>' .
|
||||||
render_calendar_picker($ctx, 'start_value', $ctx->t('Start date'), $startValue) .
|
render_calendar_picker($ctx, 'start_value', $ctx->t('Start date'), $startValue) .
|
||||||
@@ -25,6 +27,40 @@ function render_rule_form(AppContext $ctx, string $mode, ?array $rule): string
|
|||||||
'</form>';
|
'</form>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed>|null $rule */
|
||||||
|
function selected_reply_frequency(?array $rule): string
|
||||||
|
{
|
||||||
|
if ($rule === null) {
|
||||||
|
return '1440';
|
||||||
|
}
|
||||||
|
if (!empty($rule['reply_every_message'])) {
|
||||||
|
return 'every';
|
||||||
|
}
|
||||||
|
$minutes = (string)(int)($rule['repeat_minutes'] ?? 1440);
|
||||||
|
return in_array($minutes, ['30', '60', '120', '360', '720', '1440', '2880', '10080'], true) ? $minutes : '1440';
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_reply_frequency_select(AppContext $ctx, string $selected): string
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'every' => 'Send automatic reply for every message',
|
||||||
|
'30' => '30 minutes',
|
||||||
|
'60' => '1 hour',
|
||||||
|
'120' => '2 hours',
|
||||||
|
'360' => '6 hours',
|
||||||
|
'720' => '12 hours',
|
||||||
|
'1440' => '1 day',
|
||||||
|
'2880' => '2 days',
|
||||||
|
'10080' => '7 days',
|
||||||
|
];
|
||||||
|
$html = '<select name="reply_frequency">';
|
||||||
|
foreach ($options as $value => $label) {
|
||||||
|
$value = (string)$value;
|
||||||
|
$html .= '<option value="' . AppContext::e($value) . '"' . ($selected === $value ? ' selected' : '') . '>' . AppContext::e($ctx->t($label)) . '</option>';
|
||||||
|
}
|
||||||
|
return $html . '</select>';
|
||||||
|
}
|
||||||
|
|
||||||
/** @param array<string,mixed> $post @return array<string,mixed> */
|
/** @param array<string,mixed> $post @return array<string,mixed> */
|
||||||
function normalize_rule_post(array $post): array
|
function normalize_rule_post(array $post): array
|
||||||
{
|
{
|
||||||
@@ -104,7 +140,8 @@ function render_calendar_picker(AppContext $ctx, string $name, string $title, st
|
|||||||
|
|
||||||
function render_preview_box(AppContext $ctx, array $rule): string
|
function render_preview_box(AppContext $ctx, array $rule): string
|
||||||
{
|
{
|
||||||
return '<div class="preview-box"><div><strong>' . AppContext::e($ctx->t('Subject')) . ':</strong> ' . AppContext::e((string)$rule['subject']) . '</div><br><div>' . nl2br(AppContext::e((string)$rule['body'])) . '</div></div>';
|
$subjectPrefix = (string)($rule['subject_prefix'] ?? $rule['subject'] ?? '');
|
||||||
|
return '<div class="preview-box"><div><strong>' . AppContext::e($ctx->t('Subject prefix')) . ':</strong> ' . AppContext::e($subjectPrefix) . '</div><br><div>' . nl2br(AppContext::e((string)$rule['body'])) . '</div></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function render_weekday_headers(AppContext $ctx): string
|
function render_weekday_headers(AppContext $ctx): string
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ return static function (AppContext $ctx): void {
|
|||||||
$ctx->requireCsrf('delete:' . $id);
|
$ctx->requireCsrf('delete:' . $id);
|
||||||
$ctx->rules->delete($ctx->daUser->username(), $id);
|
$ctx->rules->delete($ctx->daUser->username(), $id);
|
||||||
$ctx->syncActiveBackend();
|
$ctx->syncActiveBackend();
|
||||||
$ctx->audit->record('delete', $ctx->daUser->username(), ['rule_id' => $id, 'subject' => (string)$rule['subject']]);
|
$ctx->audit->record('delete', $ctx->daUser->username(), ['rule_id' => $id, 'subject_prefix' => (string)($rule['subject_prefix'] ?? $rule['subject'] ?? '')]);
|
||||||
Http::redirect($ctx->url('index.html', ['status' => 'deleted']));
|
Http::redirect($ctx->url('index.html', ['status' => 'deleted']));
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$ctx->render('Confirm deletion', '<div class="alert alert-err">' . AppContext::e($e->getMessage()) . '</div>');
|
$ctx->render('Confirm deletion', '<div class="alert alert-err">' . AppContext::e($e->getMessage()) . '</div>');
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ return static function (AppContext $ctx): void {
|
|||||||
foreach ($rules as $rule) {
|
foreach ($rules as $rule) {
|
||||||
$id = (string)$rule['id'];
|
$id = (string)$rule['id'];
|
||||||
$enabled = !empty($rule['enabled']);
|
$enabled = !empty($rule['enabled']);
|
||||||
|
$subjectPrefix = (string)($rule['subject_prefix'] ?? $rule['subject'] ?? '');
|
||||||
$rows .= '<tr><td><span class="badge ' . ($enabled ? 'ok' : 'off') . '">' . AppContext::e($ctx->t($enabled ? 'Enabled' : 'Disabled')) . '</span></td>';
|
$rows .= '<tr><td><span class="badge ' . ($enabled ? 'ok' : 'off') . '">' . AppContext::e($ctx->t($enabled ? 'Enabled' : 'Disabled')) . '</span></td>';
|
||||||
$rows .= '<td>' . AppContext::e((string)$rule['subject']) . '<br><span class="muted">' . date('Y-m-d H:i', (int)($rule['updated_at'] ?? time())) . '</span></td>';
|
$rows .= '<td>' . AppContext::e($subjectPrefix) . '<br><span class="muted">' . date('Y-m-d H:i', (int)($rule['updated_at'] ?? time())) . '</span></td>';
|
||||||
$rows .= '<td>' . AppContext::e(date('Y-m-d H:i', (int)$rule['start_ts'])) . '<br><span class="muted">' . AppContext::e($ctx->t('To')) . ' ' . AppContext::e(date('Y-m-d H:i', (int)$rule['end_ts'])) . '</span></td>';
|
$rows .= '<td>' . AppContext::e(date('Y-m-d H:i', (int)$rule['start_ts'])) . '<br><span class="muted">' . AppContext::e($ctx->t('To')) . ' ' . AppContext::e(date('Y-m-d H:i', (int)$rule['end_ts'])) . '</span></td>';
|
||||||
$rows .= '<td>' . AppContext::e($ctx->t($rule['dynamic_scope'] ? 'Dynamic' : 'Snapshot')) . '</td><td><span class="table-actions">';
|
$rows .= '<td>' . AppContext::e($ctx->t($rule['dynamic_scope'] ? 'Dynamic' : 'Snapshot')) . '</td><td><span class="table-actions">';
|
||||||
$rows .= '<a class="btn" href="' . AppContext::e($ctx->url('update.html', ['id' => $id])) . '">' . AppContext::e($ctx->t('Edit')) . '</a>';
|
$rows .= '<a class="btn" href="' . AppContext::e($ctx->url('update.html', ['id' => $id])) . '">' . AppContext::e($ctx->t('Edit')) . '</a>';
|
||||||
@@ -35,6 +36,6 @@ return static function (AppContext $ctx): void {
|
|||||||
|
|
||||||
$body = '<div class="panel"><div class="panel-head"><div><h3>' . AppContext::e($ctx->t('Global autoresponders')) . '</h3><p class="muted">' . AppContext::e($ctx->t('All valid POP/IMAP mailboxes in this account')) . '</p></div>' .
|
$body = '<div class="panel"><div class="panel-head"><div><h3>' . AppContext::e($ctx->t('Global autoresponders')) . '</h3><p class="muted">' . AppContext::e($ctx->t('All valid POP/IMAP mailboxes in this account')) . '</p></div>' .
|
||||||
'<a class="btn amber" href="' . AppContext::e($ctx->url('create.html')) . '">' . AppContext::e($ctx->t('Add autoresponder')) . '</a></div>' .
|
'<a class="btn amber" href="' . AppContext::e($ctx->url('create.html')) . '">' . AppContext::e($ctx->t('Add autoresponder')) . '</a></div>' .
|
||||||
'<table><thead><tr><th>' . AppContext::e($ctx->t('Status')) . '</th><th>' . AppContext::e($ctx->t('Subject')) . '</th><th>' . AppContext::e($ctx->t('Sending period')) . '</th><th>' . AppContext::e($ctx->t('Scope')) . '</th><th>' . AppContext::e($ctx->t('Actions')) . '</th></tr></thead><tbody>' . $rows . '</tbody></table></div>';
|
'<table><thead><tr><th>' . AppContext::e($ctx->t('Status')) . '</th><th>' . AppContext::e($ctx->t('Subject prefix')) . '</th><th>' . AppContext::e($ctx->t('Sending period')) . '</th><th>' . AppContext::e($ctx->t('Scope')) . '</th><th>' . AppContext::e($ctx->t('Actions')) . '</th></tr></thead><tbody>' . $rows . '</tbody></table></div>';
|
||||||
$ctx->render('Global autoresponders', $body, $ok);
|
$ctx->render('Global autoresponders', $body, $ok);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,13 +24,18 @@ class DirectAdminVacationApi
|
|||||||
[$endDate, $endTime] = $this->splitPeriod((string)($rule['end_value'] ?? ''));
|
[$endDate, $endTime] = $this->splitPeriod((string)($rule['end_value'] ?? ''));
|
||||||
[$startYear, $startMonth, $startDay] = explode('-', $startDate);
|
[$startYear, $startMonth, $startDay] = explode('-', $startDate);
|
||||||
[$endYear, $endMonth, $endDay] = explode('-', $endDate);
|
[$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 [
|
return [
|
||||||
'action' => $exists ? 'modify' : 'create',
|
'action' => $exists ? 'modify' : 'create',
|
||||||
'domain' => $domain,
|
'domain' => $domain,
|
||||||
'user' => $local,
|
'user' => $local,
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'text' => (string)($rule['body'] ?? ''),
|
'text' => (string)($rule['body'] ?? ''),
|
||||||
'subject' => (string)($rule['subject'] ?? ''),
|
'subject_prefix' => $subjectPrefix,
|
||||||
|
'subject' => $subjectPrefix,
|
||||||
|
'reply_frequency' => $replyFrequency,
|
||||||
|
'reply_once_time' => $replyFrequency,
|
||||||
'starttime' => $startTime,
|
'starttime' => $startTime,
|
||||||
'startyear' => $startYear,
|
'startyear' => $startYear,
|
||||||
'startmonth' => $startMonth,
|
'startmonth' => $startMonth,
|
||||||
|
|||||||
@@ -3,18 +3,27 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
final class RuleValidator
|
final class RuleValidator
|
||||||
{
|
{
|
||||||
|
/** @var array<string,int> */
|
||||||
|
private const REPLY_FREQUENCIES = [
|
||||||
|
'30' => 30,
|
||||||
|
'60' => 60,
|
||||||
|
'120' => 120,
|
||||||
|
'360' => 360,
|
||||||
|
'720' => 720,
|
||||||
|
'1440' => 1440,
|
||||||
|
'2880' => 2880,
|
||||||
|
'10080' => 10080,
|
||||||
|
];
|
||||||
|
|
||||||
/** @param array<string,mixed> $input @return array<string,mixed> */
|
/** @param array<string,mixed> $input @return array<string,mixed> */
|
||||||
public static function validate(array $input, Settings $settings): array
|
public static function validate(array $input, Settings $settings): array
|
||||||
{
|
{
|
||||||
$subject = trim((string)($input['subject'] ?? ''));
|
$subjectPrefix = trim((string)($input['subject_prefix'] ?? $input['subject'] ?? ''));
|
||||||
if ($subject === '') {
|
if (str_contains($subjectPrefix, "\r") || str_contains($subjectPrefix, "\n")) {
|
||||||
throw new InvalidArgumentException('Temat jest wymagany.');
|
throw new InvalidArgumentException('Przedrostek tematu nie może zawierać nowych linii.');
|
||||||
}
|
}
|
||||||
if (str_contains($subject, "\r") || str_contains($subject, "\n")) {
|
if (strlen($subjectPrefix) > $settings->maxSubjectBytes()) {
|
||||||
throw new InvalidArgumentException('Temat nie może zawierać nowych linii.');
|
throw new InvalidArgumentException('Przedrostek tematu jest za długi.');
|
||||||
}
|
|
||||||
if (strlen($subject) > $settings->maxSubjectBytes()) {
|
|
||||||
throw new InvalidArgumentException('Temat jest za długi.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$body = str_replace(["\r\n", "\r", "\0"], ["\n", "\n", ''], (string)($input['body'] ?? ''));
|
$body = str_replace(["\r\n", "\r", "\0"], ["\n", "\n", ''], (string)($input['body'] ?? ''));
|
||||||
@@ -33,9 +42,10 @@ final class RuleValidator
|
|||||||
if ($endTs <= $startTs) {
|
if ($endTs <= $startTs) {
|
||||||
throw new InvalidArgumentException('Data zakończenia musi być późniejsza niż data rozpoczęcia.');
|
throw new InvalidArgumentException('Data zakończenia musi być późniejsza niż data rozpoczęcia.');
|
||||||
}
|
}
|
||||||
|
[$replyEveryMessage, $repeatMinutes] = self::parseReplyFrequency((string)($input['reply_frequency'] ?? '1440'));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'subject' => $subject,
|
'subject_prefix' => $subjectPrefix,
|
||||||
'body' => $body,
|
'body' => $body,
|
||||||
'schedule_mode' => $mode,
|
'schedule_mode' => $mode,
|
||||||
'timezone' => $settings->timezone(),
|
'timezone' => $settings->timezone(),
|
||||||
@@ -44,10 +54,25 @@ final class RuleValidator
|
|||||||
'start_ts' => $startTs,
|
'start_ts' => $startTs,
|
||||||
'end_ts' => $endTs,
|
'end_ts' => $endTs,
|
||||||
'enabled' => self::truthy($input['enabled'] ?? false),
|
'enabled' => self::truthy($input['enabled'] ?? false),
|
||||||
|
'reply_every_message' => $replyEveryMessage,
|
||||||
|
'repeat_minutes' => $repeatMinutes,
|
||||||
'dynamic_scope' => $settings->dynamicMailboxScope(),
|
'dynamic_scope' => $settings->dynamicMailboxScope(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array{0:bool,1:int} */
|
||||||
|
private static function parseReplyFrequency(string $value): array
|
||||||
|
{
|
||||||
|
$value = trim(strtolower($value));
|
||||||
|
if ($value === 'every') {
|
||||||
|
return [true, 0];
|
||||||
|
}
|
||||||
|
if (isset(self::REPLY_FREQUENCIES[$value])) {
|
||||||
|
return [false, self::REPLY_FREQUENCIES[$value]];
|
||||||
|
}
|
||||||
|
throw new InvalidArgumentException('Częstość odpowiedzi jest nieprawidłowa.');
|
||||||
|
}
|
||||||
|
|
||||||
/** @return array{0:int,1:string} */
|
/** @return array{0:int,1:string} */
|
||||||
private static function parseScheduleValue(string $value, string $mode, DateTimeZone $tz, bool $start): array
|
private static function parseScheduleValue(string $value, string $mode, DateTimeZone $tz, bool $start): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ final class Settings
|
|||||||
'DEFAULT_ENABLE' => 'true',
|
'DEFAULT_ENABLE' => 'true',
|
||||||
'GLOBAL_AUTORESPONDER_TIMEZONE' => 'Europe/Warsaw',
|
'GLOBAL_AUTORESPONDER_TIMEZONE' => 'Europe/Warsaw',
|
||||||
'GLOBAL_AUTORESPONDER_BACKEND' => 'da',
|
'GLOBAL_AUTORESPONDER_BACKEND' => 'da',
|
||||||
'GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE' => 'false',
|
|
||||||
'GLOBAL_AUTORESPONDER_REPEAT_MINUTES' => '1440',
|
|
||||||
'GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE' => 'true',
|
'GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE' => 'true',
|
||||||
'GLOBAL_AUTORESPONDER_MAX_SUBJECT_BYTES' => '255',
|
'GLOBAL_AUTORESPONDER_MAX_SUBJECT_BYTES' => '255',
|
||||||
'GLOBAL_AUTORESPONDER_MAX_BODY_BYTES' => '20000',
|
'GLOBAL_AUTORESPONDER_MAX_BODY_BYTES' => '20000',
|
||||||
@@ -103,16 +101,6 @@ final class Settings
|
|||||||
return $this->backendMode() === 'da' ? 'directadmin_period' : 'exact_hour';
|
return $this->backendMode() === 'da' ? 'directadmin_period' : 'exact_hour';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function replyEveryMessage(): bool
|
|
||||||
{
|
|
||||||
return $this->getBool('GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function repeatMinutes(): int
|
|
||||||
{
|
|
||||||
return $this->getInt('GLOBAL_AUTORESPONDER_REPEAT_MINUTES', 1440);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dynamicMailboxScope(): bool
|
public function dynamicMailboxScope(): bool
|
||||||
{
|
{
|
||||||
return $this->getBool('GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE', true);
|
return $this->getBool('GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE', true);
|
||||||
@@ -156,9 +144,6 @@ final class Settings
|
|||||||
if (!in_array($this->backendMode(), ['da', 'exim'], true)) {
|
if (!in_array($this->backendMode(), ['da', 'exim'], true)) {
|
||||||
throw new InvalidArgumentException('Invalid backend mode: ' . $this->backendMode());
|
throw new InvalidArgumentException('Invalid backend mode: ' . $this->backendMode());
|
||||||
}
|
}
|
||||||
if ($this->repeatMinutes() < 0) {
|
|
||||||
throw new InvalidArgumentException('Invalid repeat minutes');
|
|
||||||
}
|
|
||||||
if ($this->maxSubjectBytes() < 1 || $this->maxBodyBytes() < 1) {
|
if ($this->maxSubjectBytes() < 1 || $this->maxBodyBytes() < 1) {
|
||||||
throw new InvalidArgumentException('Invalid size limits');
|
throw new InvalidArgumentException('Invalid size limits');
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -11,8 +11,20 @@ return [
|
|||||||
'Edit autoresponder' => 'Edit autoresponder',
|
'Edit autoresponder' => 'Edit autoresponder',
|
||||||
'Preview autoresponder' => 'Preview autoresponder',
|
'Preview autoresponder' => 'Preview autoresponder',
|
||||||
'Subject' => 'Subject',
|
'Subject' => 'Subject',
|
||||||
|
'Subject prefix' => 'Subject prefix',
|
||||||
|
'Original subject suffix' => ': original subject',
|
||||||
'Message body' => 'Message body',
|
'Message body' => 'Message body',
|
||||||
'Response content' => 'Response content',
|
'Response content' => 'Response content',
|
||||||
|
'Response frequency' => 'Response frequency',
|
||||||
|
'Send automatic reply for every message' => 'Send automatic reply for every message',
|
||||||
|
'30 minutes' => '30 minutes',
|
||||||
|
'1 hour' => '1 hour',
|
||||||
|
'2 hours' => '2 hours',
|
||||||
|
'6 hours' => '6 hours',
|
||||||
|
'12 hours' => '12 hours',
|
||||||
|
'1 day' => '1 day',
|
||||||
|
'2 days' => '2 days',
|
||||||
|
'7 days' => '7 days',
|
||||||
'Status' => 'Status',
|
'Status' => 'Status',
|
||||||
'Enabled' => 'Enabled',
|
'Enabled' => 'Enabled',
|
||||||
'Disabled' => 'Disabled',
|
'Disabled' => 'Disabled',
|
||||||
|
|||||||
+12
@@ -11,8 +11,20 @@ return [
|
|||||||
'Edit autoresponder' => 'Edytuj autoresponder',
|
'Edit autoresponder' => 'Edytuj autoresponder',
|
||||||
'Preview autoresponder' => 'Podgląd autorespondera',
|
'Preview autoresponder' => 'Podgląd autorespondera',
|
||||||
'Subject' => 'Temat',
|
'Subject' => 'Temat',
|
||||||
|
'Subject prefix' => 'Przedrostek tematu',
|
||||||
|
'Original subject suffix' => ': pierwotny temat',
|
||||||
'Message body' => 'Treść wiadomości',
|
'Message body' => 'Treść wiadomości',
|
||||||
'Response content' => 'Treść odpowiedzi',
|
'Response content' => 'Treść odpowiedzi',
|
||||||
|
'Response frequency' => 'Częstość odpowiedzi',
|
||||||
|
'Send automatic reply for every message' => 'Wysyłaj automatyczną odpowiedź dla każdej wiadomości',
|
||||||
|
'30 minutes' => '30 minut',
|
||||||
|
'1 hour' => '1 godzina',
|
||||||
|
'2 hours' => '2 godziny',
|
||||||
|
'6 hours' => '6 godzin',
|
||||||
|
'12 hours' => '12 godzin',
|
||||||
|
'1 day' => '1 dzień',
|
||||||
|
'2 days' => '2 dni',
|
||||||
|
'7 days' => '7 dni',
|
||||||
'Status' => 'Status',
|
'Status' => 'Status',
|
||||||
'Enabled' => 'Włączony',
|
'Enabled' => 'Włączony',
|
||||||
'Disabled' => 'Wyłączony',
|
'Disabled' => 'Wyłączony',
|
||||||
|
|||||||
@@ -13,18 +13,11 @@ GLOBAL_AUTORESPONDER_TIMEZONE=Europe/Warsaw
|
|||||||
# exim = własna obsługa z możliwością podania dokładnej godziny i minuty.
|
# exim = własna obsługa z możliwością podania dokładnej godziny i minuty.
|
||||||
GLOBAL_AUTORESPONDER_BACKEND=da
|
GLOBAL_AUTORESPONDER_BACKEND=da
|
||||||
|
|
||||||
# Czy odpowiadać automatycznie na każdą kwalifikującą się wiadomość.
|
|
||||||
GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE=false
|
|
||||||
|
|
||||||
# Minimalny odstęp w minutach między autoresponderami do tego samego nadawcy,
|
|
||||||
# używany tylko gdy GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE=false.
|
|
||||||
GLOBAL_AUTORESPONDER_REPEAT_MINUTES=1440
|
|
||||||
|
|
||||||
# Czy reguła obejmuje także skrzynki POP/IMAP dodane po utworzeniu reguły.
|
# Czy reguła obejmuje także skrzynki POP/IMAP dodane po utworzeniu reguły.
|
||||||
# true = zakres dynamiczny, false = snapshot skrzynek przy tworzeniu/edycji.
|
# true = zakres dynamiczny, false = snapshot skrzynek przy tworzeniu/edycji.
|
||||||
GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE=true
|
GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE=true
|
||||||
|
|
||||||
# Maksymalna długość tematu po przycięciu białych znaków.
|
# Maksymalna długość przedrostka tematu po przycięciu białych znaków.
|
||||||
GLOBAL_AUTORESPONDER_MAX_SUBJECT_BYTES=255
|
GLOBAL_AUTORESPONDER_MAX_SUBJECT_BYTES=255
|
||||||
|
|
||||||
# Maksymalna długość treści wiadomości po normalizacji.
|
# Maksymalna długość treści wiadomości po normalizacji.
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ name=global-autoresponder
|
|||||||
id=global-autoresponder
|
id=global-autoresponder
|
||||||
type=user
|
type=user
|
||||||
author=HITME.PL
|
author=HITME.PL
|
||||||
version=1.0.7
|
version=1.0.8
|
||||||
active=no
|
active=no
|
||||||
installed=no
|
installed=no
|
||||||
user_run_as=root
|
user_run_as=root
|
||||||
|
|||||||
@@ -55,15 +55,19 @@ final class GlobalAutoresponderWorker
|
|||||||
if ($ruleId === '') {
|
if ($ruleId === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$repeatMinutes = $this->settings->replyEveryMessage() ? 0 : $this->settings->repeatMinutes();
|
$repeatMinutes = !empty($rule['reply_every_message']) ? 0 : max(0, (int)($rule['repeat_minutes'] ?? 1440));
|
||||||
if (!$this->repeat->shouldSend($owner, $ruleId, $recipient, $sender, $repeatMinutes)) {
|
if (!$this->repeat->shouldSend($owner, $ruleId, $recipient, $sender, $repeatMinutes)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
$replySubject = self::replySubject(
|
||||||
|
(string)($rule['subject_prefix'] ?? $rule['subject'] ?? ''),
|
||||||
|
self::headerValue($headers, 'subject')
|
||||||
|
);
|
||||||
$ok = (bool)call_user_func(
|
$ok = (bool)call_user_func(
|
||||||
$this->send,
|
$this->send,
|
||||||
$recipient,
|
$recipient,
|
||||||
strtolower($sender),
|
strtolower($sender),
|
||||||
(string)$rule['subject'],
|
$replySubject,
|
||||||
(string)$rule['body']
|
(string)$rule['body']
|
||||||
);
|
);
|
||||||
if ($ok) {
|
if ($ok) {
|
||||||
@@ -155,6 +159,30 @@ final class GlobalAutoresponderWorker
|
|||||||
}
|
}
|
||||||
return $headers;
|
return $headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string,string> $headers */
|
||||||
|
private static function headerValue(array $headers, string $name): string
|
||||||
|
{
|
||||||
|
foreach ($headers as $key => $value) {
|
||||||
|
if (strcasecmp($key, $name) === 0) {
|
||||||
|
return trim($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function replySubject(string $prefix, string $originalSubject): string
|
||||||
|
{
|
||||||
|
$prefix = trim($prefix);
|
||||||
|
$originalSubject = trim($originalSubject);
|
||||||
|
if ($prefix !== '' && $originalSubject !== '') {
|
||||||
|
return str_ends_with($prefix, ':') ? $prefix . ' ' . $originalSubject : $prefix . ': ' . $originalSubject;
|
||||||
|
}
|
||||||
|
if ($prefix !== '') {
|
||||||
|
return $prefix;
|
||||||
|
}
|
||||||
|
return $originalSubject !== '' ? $originalSubject : 'Automatic reply';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PHP_SAPI === 'cli' && realpath((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) {
|
if (PHP_SAPI === 'cli' && realpath((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) {
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ test('settings validate defaults and external paths', function (): void {
|
|||||||
'DEFAULT_ENABLE=false',
|
'DEFAULT_ENABLE=false',
|
||||||
'GLOBAL_AUTORESPONDER_TIMEZONE=Europe/Warsaw',
|
'GLOBAL_AUTORESPONDER_TIMEZONE=Europe/Warsaw',
|
||||||
'GLOBAL_AUTORESPONDER_BACKEND=exim',
|
'GLOBAL_AUTORESPONDER_BACKEND=exim',
|
||||||
'GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE=false',
|
|
||||||
'GLOBAL_AUTORESPONDER_REPEAT_MINUTES=60',
|
|
||||||
'GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE=true',
|
'GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE=true',
|
||||||
'GLOBAL_AUTORESPONDER_MAX_SUBJECT_BYTES=255',
|
'GLOBAL_AUTORESPONDER_MAX_SUBJECT_BYTES=255',
|
||||||
'GLOBAL_AUTORESPONDER_MAX_BODY_BYTES=20000',
|
'GLOBAL_AUTORESPONDER_MAX_BODY_BYTES=20000',
|
||||||
@@ -24,11 +22,16 @@ test('settings validate defaults and external paths', function (): void {
|
|||||||
assert_same('Europe/Warsaw', $settings->timezone());
|
assert_same('Europe/Warsaw', $settings->timezone());
|
||||||
assert_same('exact_hour', $settings->scheduleMode());
|
assert_same('exact_hour', $settings->scheduleMode());
|
||||||
assert_same('exim', $settings->backendMode());
|
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/config', Settings::CONFIG_DIR);
|
||||||
assert_same('/usr/local/hitme_plugins/global-autoresponders/data', Settings::DATA_DIR);
|
assert_same('/usr/local/hitme_plugins/global-autoresponders/data', Settings::DATA_DIR);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('settings template does not contain global repeat policy settings', function (): void {
|
||||||
|
$template = file_get_contents(PLUGIN_ROOT . '/plugin-settings.conf') ?: '';
|
||||||
|
assert_false(str_contains($template, 'GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE'));
|
||||||
|
assert_false(str_contains($template, 'GLOBAL_AUTORESPONDER_REPEAT_MINUTES'));
|
||||||
|
});
|
||||||
|
|
||||||
test('settings reject invalid timezone and schedule mode', function (): void {
|
test('settings reject invalid timezone and schedule mode', function (): void {
|
||||||
$path = TEST_TMP . '/bad-settings.conf';
|
$path = TEST_TMP . '/bad-settings.conf';
|
||||||
test_write($path, "GLOBAL_AUTORESPONDER_TIMEZONE=Not/AZone\nGLOBAL_AUTORESPONDER_BACKEND=bad\n");
|
test_write($path, "GLOBAL_AUTORESPONDER_TIMEZONE=Not/AZone\nGLOBAL_AUTORESPONDER_BACKEND=bad\n");
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ require_once PLUGIN_ROOT . '/exec/lib/DirectAdminVacationApi.php';
|
|||||||
test('directadmin vacation api builds scoped payload for one mailbox', function (): void {
|
test('directadmin vacation api builds scoped payload for one mailbox', function (): void {
|
||||||
$api = new DirectAdminVacationApi('/usr/local/directadmin/directadmin');
|
$api = new DirectAdminVacationApi('/usr/local/directadmin/directadmin');
|
||||||
$payload = $api->buildSavePayload('info@example.com', [
|
$payload = $api->buildSavePayload('info@example.com', [
|
||||||
'subject' => 'Urlop',
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_every_message' => false,
|
||||||
|
'repeat_minutes' => 1440,
|
||||||
'body' => 'Body',
|
'body' => 'Body',
|
||||||
'start_value' => '2026-06-10|morning',
|
'start_value' => '2026-06-10|morning',
|
||||||
'end_value' => '2026-06-24|evening',
|
'end_value' => '2026-06-24|evening',
|
||||||
@@ -15,6 +17,8 @@ test('directadmin vacation api builds scoped payload for one mailbox', function
|
|||||||
assert_same('info', $payload['user']);
|
assert_same('info', $payload['user']);
|
||||||
assert_same('example.com', $payload['domain']);
|
assert_same('example.com', $payload['domain']);
|
||||||
assert_same('Body', $payload['text']);
|
assert_same('Body', $payload['text']);
|
||||||
|
assert_same('Urlop', $payload['subject_prefix']);
|
||||||
|
assert_same('1440', $payload['reply_frequency']);
|
||||||
assert_same('morning', $payload['starttime']);
|
assert_same('morning', $payload['starttime']);
|
||||||
assert_same('evening', $payload['endtime']);
|
assert_same('evening', $payload['endtime']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ test('directadmin vacation sync creates tracked entries for enabled rule mailbox
|
|||||||
|
|
||||||
$repo = new RuleRepository(TEST_TMP . '/da-sync-data');
|
$repo = new RuleRepository(TEST_TMP . '/da-sync-data');
|
||||||
$rule = $repo->create('alice', [
|
$rule = $repo->create('alice', [
|
||||||
'subject' => 'Urlop',
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_every_message' => false,
|
||||||
|
'repeat_minutes' => 1440,
|
||||||
'body' => 'Body',
|
'body' => 'Body',
|
||||||
'start_value' => '2026-06-10|morning',
|
'start_value' => '2026-06-10|morning',
|
||||||
'end_value' => '2026-06-24|evening',
|
'end_value' => '2026-06-24|evening',
|
||||||
@@ -34,7 +36,7 @@ test('directadmin vacation sync creates tracked entries for enabled rule mailbox
|
|||||||
}
|
}
|
||||||
public function saveVacation(string $owner, string $email, array $rule, bool $exists): void
|
public function saveVacation(string $owner, string $email, array $rule, bool $exists): void
|
||||||
{
|
{
|
||||||
$this->calls[] = ['save', $owner, $email, $exists, $rule['subject']];
|
$this->calls[] = ['save', $owner, $email, $exists, $rule['subject_prefix']];
|
||||||
}
|
}
|
||||||
public function deleteVacation(string $owner, string $email): void
|
public function deleteVacation(string $owner, string $email): void
|
||||||
{
|
{
|
||||||
@@ -68,7 +70,9 @@ test('directadmin vacation sync deletes stale tracked entries after rule removal
|
|||||||
$repo = new RuleRepository($dataDir);
|
$repo = new RuleRepository($dataDir);
|
||||||
$syncRepo = new DirectAdminSyncRepository($dataDir);
|
$syncRepo = new DirectAdminSyncRepository($dataDir);
|
||||||
$rule = $repo->create('alice', [
|
$rule = $repo->create('alice', [
|
||||||
'subject' => 'Urlop',
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_every_message' => false,
|
||||||
|
'repeat_minutes' => 1440,
|
||||||
'body' => 'Body',
|
'body' => 'Body',
|
||||||
'start_value' => '2026-06-10|morning',
|
'start_value' => '2026-06-10|morning',
|
||||||
'end_value' => '2026-06-24|evening',
|
'end_value' => '2026-06-24|evening',
|
||||||
@@ -107,7 +111,9 @@ test('directadmin vacation sync refuses to overwrite untracked native vacation',
|
|||||||
$dataDir = TEST_TMP . '/da-sync-conflict-data';
|
$dataDir = TEST_TMP . '/da-sync-conflict-data';
|
||||||
$repo = new RuleRepository($dataDir);
|
$repo = new RuleRepository($dataDir);
|
||||||
$repo->create('alice', [
|
$repo->create('alice', [
|
||||||
'subject' => 'Urlop',
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_every_message' => false,
|
||||||
|
'repeat_minutes' => 1440,
|
||||||
'body' => 'Body',
|
'body' => 'Body',
|
||||||
'start_value' => '2026-06-10|morning',
|
'start_value' => '2026-06-10|morning',
|
||||||
'end_value' => '2026-06-24|evening',
|
'end_value' => '2026-06-24|evening',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ require_once PLUGIN_ROOT . '/scripts/global_autoresponder_worker.php';
|
|||||||
|
|
||||||
test('exim worker sends active rule for resolved mailbox only', function (): void {
|
test('exim worker sends active rule for resolved mailbox only', function (): void {
|
||||||
$settingsPath = TEST_TMP . '/worker-settings.conf';
|
$settingsPath = TEST_TMP . '/worker-settings.conf';
|
||||||
test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=exim\nGLOBAL_AUTORESPONDER_REPEAT_MINUTES=60\n");
|
test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=exim\n");
|
||||||
$settings = Settings::load($settingsPath);
|
$settings = Settings::load($settingsPath);
|
||||||
|
|
||||||
$virtualRoot = TEST_TMP . '/worker-virtual';
|
$virtualRoot = TEST_TMP . '/worker-virtual';
|
||||||
@@ -15,6 +15,9 @@ test('exim worker sends active rule for resolved mailbox only', function (): voi
|
|||||||
$repo = new RuleRepository(TEST_TMP . '/worker-data');
|
$repo = new RuleRepository(TEST_TMP . '/worker-data');
|
||||||
$created = $repo->create('alice', [
|
$created = $repo->create('alice', [
|
||||||
'subject' => 'Urlop',
|
'subject' => 'Urlop',
|
||||||
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_every_message' => false,
|
||||||
|
'repeat_minutes' => 60,
|
||||||
'body' => 'Odpowiem później',
|
'body' => 'Odpowiem później',
|
||||||
'start_ts' => 900,
|
'start_ts' => 900,
|
||||||
'end_ts' => 1100,
|
'end_ts' => 1100,
|
||||||
@@ -42,7 +45,7 @@ test('exim worker sends active rule for resolved mailbox only', function (): voi
|
|||||||
assert_same(1, $count);
|
assert_same(1, $count);
|
||||||
assert_same('info@example.com', $sent[0]['from']);
|
assert_same('info@example.com', $sent[0]['from']);
|
||||||
assert_same('person@example.net', $sent[0]['to']);
|
assert_same('person@example.net', $sent[0]['to']);
|
||||||
assert_same('Urlop', $sent[0]['subject']);
|
assert_same('Urlop: hello', $sent[0]['subject']);
|
||||||
assert_same('Odpowiem później', $sent[0]['body']);
|
assert_same('Odpowiem później', $sent[0]['body']);
|
||||||
|
|
||||||
$count = $worker->handle(
|
$count = $worker->handle(
|
||||||
@@ -56,7 +59,7 @@ test('exim worker sends active rule for resolved mailbox only', function (): voi
|
|||||||
|
|
||||||
test('exim worker suppresses automated messages and ignores unresolved recipients', function (): void {
|
test('exim worker suppresses automated messages and ignores unresolved recipients', function (): void {
|
||||||
$settingsPath = TEST_TMP . '/worker-suppress-settings.conf';
|
$settingsPath = TEST_TMP . '/worker-suppress-settings.conf';
|
||||||
test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=exim\nGLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE=true\n");
|
test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=exim\n");
|
||||||
$settings = Settings::load($settingsPath);
|
$settings = Settings::load($settingsPath);
|
||||||
|
|
||||||
$virtualRoot = TEST_TMP . '/worker-suppress-virtual';
|
$virtualRoot = TEST_TMP . '/worker-suppress-virtual';
|
||||||
@@ -66,7 +69,9 @@ test('exim worker suppresses automated messages and ignores unresolved recipient
|
|||||||
|
|
||||||
$repo = new RuleRepository(TEST_TMP . '/worker-suppress-data');
|
$repo = new RuleRepository(TEST_TMP . '/worker-suppress-data');
|
||||||
$repo->create('alice', [
|
$repo->create('alice', [
|
||||||
'subject' => 'Urlop',
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_every_message' => true,
|
||||||
|
'repeat_minutes' => 0,
|
||||||
'body' => 'Body',
|
'body' => 'Body',
|
||||||
'start_ts' => 1,
|
'start_ts' => 1,
|
||||||
'end_ts' => 2000,
|
'end_ts' => 2000,
|
||||||
@@ -99,7 +104,7 @@ test('exim worker suppresses automated messages and ignores unresolved recipient
|
|||||||
|
|
||||||
test('exim worker respects static mailbox snapshot scope', function (): void {
|
test('exim worker respects static mailbox snapshot scope', function (): void {
|
||||||
$settingsPath = TEST_TMP . '/worker-snapshot-settings.conf';
|
$settingsPath = TEST_TMP . '/worker-snapshot-settings.conf';
|
||||||
test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=exim\nGLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE=true\n");
|
test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=exim\n");
|
||||||
$settings = Settings::load($settingsPath);
|
$settings = Settings::load($settingsPath);
|
||||||
|
|
||||||
$virtualRoot = TEST_TMP . '/worker-snapshot-virtual';
|
$virtualRoot = TEST_TMP . '/worker-snapshot-virtual';
|
||||||
@@ -108,7 +113,9 @@ test('exim worker respects static mailbox snapshot scope', function (): void {
|
|||||||
|
|
||||||
$repo = new RuleRepository(TEST_TMP . '/worker-snapshot-data');
|
$repo = new RuleRepository(TEST_TMP . '/worker-snapshot-data');
|
||||||
$repo->create('alice', [
|
$repo->create('alice', [
|
||||||
'subject' => 'Urlop',
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_every_message' => true,
|
||||||
|
'repeat_minutes' => 0,
|
||||||
'body' => 'Body',
|
'body' => 'Body',
|
||||||
'start_ts' => 1,
|
'start_ts' => 1,
|
||||||
'end_ts' => 2000,
|
'end_ts' => 2000,
|
||||||
|
|||||||
+13
-5
@@ -55,7 +55,11 @@ test('rule form uses minute time input and no summary box in exim mode', functio
|
|||||||
assert_contains('type="time"', $html);
|
assert_contains('type="time"', $html);
|
||||||
assert_contains('step="60"', $html);
|
assert_contains('step="60"', $html);
|
||||||
assert_false(strpos($html, 'summary-card') !== false, 'Summary box should not be rendered in create/edit form');
|
assert_false(strpos($html, 'summary-card') !== false, 'Summary box should not be rendered in create/edit form');
|
||||||
assert_contains('Temat', $html);
|
assert_contains('Przedrostek tematu', $html);
|
||||||
|
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('1 dzień', $html);
|
||||||
assert_contains('Treść wiadomości', $html);
|
assert_contains('Treść wiadomości', $html);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,7 +103,9 @@ test('calendar picker is based on selected rule month and supports month navigat
|
|||||||
|
|
||||||
$html = render_rule_form($ctx, 'update', [
|
$html = render_rule_form($ctx, 'update', [
|
||||||
'id' => 'abc',
|
'id' => 'abc',
|
||||||
'subject' => 'Urlop',
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_every_message' => false,
|
||||||
|
'repeat_minutes' => 1440,
|
||||||
'body' => 'Body',
|
'body' => 'Body',
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'start_value' => '2027-02-14T20:00',
|
'start_value' => '2027-02-14T20:00',
|
||||||
@@ -132,8 +138,8 @@ test('rule form localizes calendar and preview labels without translating user c
|
|||||||
assert_false(strpos($html, 'Wybrano') !== false);
|
assert_false(strpos($html, 'Wybrano') !== false);
|
||||||
assert_false(strpos($html, '<th>Pn</th>') !== false);
|
assert_false(strpos($html, '<th>Pn</th>') !== false);
|
||||||
|
|
||||||
$preview = render_preview_box($ctx, ['subject' => 'Delete', 'body' => 'Cancel']);
|
$preview = render_preview_box($ctx, ['subject_prefix' => 'Delete', 'body' => 'Cancel']);
|
||||||
assert_contains('<strong>Subject:</strong> Delete', $preview);
|
assert_contains('<strong>Subject prefix:</strong> Delete', $preview);
|
||||||
assert_contains('Cancel', $preview);
|
assert_contains('Cancel', $preview);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,7 +149,9 @@ test('directadmin backend allows only one enabled global rule per account', func
|
|||||||
$settings = Settings::load($settingsPath);
|
$settings = Settings::load($settingsPath);
|
||||||
$repo = new RuleRepository(TEST_TMP . '/single-data');
|
$repo = new RuleRepository(TEST_TMP . '/single-data');
|
||||||
$repo->create('alice', [
|
$repo->create('alice', [
|
||||||
'subject' => 'Pierwsza',
|
'subject_prefix' => 'Pierwsza',
|
||||||
|
'reply_every_message' => false,
|
||||||
|
'repeat_minutes' => 1440,
|
||||||
'body' => 'Body',
|
'body' => 'Body',
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'start_value' => '2026-06-10|morning',
|
'start_value' => '2026-06-10|morning',
|
||||||
|
|||||||
@@ -9,14 +9,17 @@ test('rule validator normalizes exact hour schedule', function (): void {
|
|||||||
test_write($settingsPath, "GLOBAL_AUTORESPONDER_TIMEZONE=Europe/Warsaw\nGLOBAL_AUTORESPONDER_BACKEND=exim\n");
|
test_write($settingsPath, "GLOBAL_AUTORESPONDER_TIMEZONE=Europe/Warsaw\nGLOBAL_AUTORESPONDER_BACKEND=exim\n");
|
||||||
$settings = Settings::load($settingsPath);
|
$settings = Settings::load($settingsPath);
|
||||||
$rule = RuleValidator::validate([
|
$rule = RuleValidator::validate([
|
||||||
'subject' => 'Urlop',
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_frequency' => '120',
|
||||||
'body' => "Dzień dobry\r\nWracam jutro",
|
'body' => "Dzień dobry\r\nWracam jutro",
|
||||||
'start_value' => '2026-06-10T09:37',
|
'start_value' => '2026-06-10T09:37',
|
||||||
'end_value' => '2026-06-24T17:12',
|
'end_value' => '2026-06-24T17:12',
|
||||||
'enabled' => '1',
|
'enabled' => '1',
|
||||||
], $settings);
|
], $settings);
|
||||||
|
|
||||||
assert_same('Urlop', $rule['subject']);
|
assert_same('Urlop', $rule['subject_prefix']);
|
||||||
|
assert_false($rule['reply_every_message']);
|
||||||
|
assert_same(120, $rule['repeat_minutes']);
|
||||||
assert_same("Dzień dobry\nWracam jutro", $rule['body']);
|
assert_same("Dzień dobry\nWracam jutro", $rule['body']);
|
||||||
assert_same('exact_hour', $rule['schedule_mode']);
|
assert_same('exact_hour', $rule['schedule_mode']);
|
||||||
assert_same('2026-06-10T09:37', $rule['start_value']);
|
assert_same('2026-06-10T09:37', $rule['start_value']);
|
||||||
@@ -28,27 +31,47 @@ test('rule validator accepts da period schedule when backend is da', function ()
|
|||||||
test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=da\n");
|
test_write($settingsPath, "GLOBAL_AUTORESPONDER_BACKEND=da\n");
|
||||||
$settings = Settings::load($settingsPath);
|
$settings = Settings::load($settingsPath);
|
||||||
$rule = RuleValidator::validate([
|
$rule = RuleValidator::validate([
|
||||||
'subject' => 'Urlop',
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_frequency' => 'every',
|
||||||
'body' => 'Body',
|
'body' => 'Body',
|
||||||
'start_value' => '2026-06-10|morning',
|
'start_value' => '2026-06-10|morning',
|
||||||
'end_value' => '2026-06-24|evening',
|
'end_value' => '2026-06-24|evening',
|
||||||
'enabled' => '1',
|
'enabled' => '1',
|
||||||
], $settings);
|
], $settings);
|
||||||
assert_same('directadmin_period', $rule['schedule_mode']);
|
assert_same('directadmin_period', $rule['schedule_mode']);
|
||||||
|
assert_true($rule['reply_every_message']);
|
||||||
|
assert_same(0, $rule['repeat_minutes']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rule validator rejects header injection and reversed dates', function (): void {
|
test('rule validator rejects header injection and reversed dates', function (): void {
|
||||||
$settings = Settings::load(TEST_TMP . '/missing.conf');
|
$settings = Settings::load(TEST_TMP . '/missing.conf');
|
||||||
try {
|
try {
|
||||||
RuleValidator::validate([
|
RuleValidator::validate([
|
||||||
'subject' => "Bad\nSubject",
|
'subject_prefix' => "Bad\nSubject",
|
||||||
'body' => 'Body',
|
'body' => 'Body',
|
||||||
'start_value' => '2026-06-24T17:00',
|
'start_value' => '2026-06-24T17:00',
|
||||||
'end_value' => '2026-06-10T09:00',
|
'end_value' => '2026-06-10T09:00',
|
||||||
], $settings);
|
], $settings);
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (InvalidArgumentException $e) {
|
||||||
assert_contains('Temat', $e->getMessage());
|
assert_contains('Przedrostek tematu', $e->getMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new RuntimeException('Expected validation failure');
|
throw new RuntimeException('Expected validation failure');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rule validator rejects unsupported response frequency', function (): void {
|
||||||
|
$settings = Settings::load(TEST_TMP . '/missing-frequency.conf');
|
||||||
|
try {
|
||||||
|
RuleValidator::validate([
|
||||||
|
'subject_prefix' => 'Urlop',
|
||||||
|
'reply_frequency' => '45',
|
||||||
|
'body' => 'Body',
|
||||||
|
'start_value' => '2026-06-10|morning',
|
||||||
|
'end_value' => '2026-06-24|evening',
|
||||||
|
], $settings);
|
||||||
|
} catch (InvalidArgumentException $e) {
|
||||||
|
assert_contains('Częstość odpowiedzi', $e->getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new RuntimeException('Expected invalid frequency to fail');
|
||||||
|
});
|
||||||
|
|||||||
+5
-2
@@ -33,8 +33,11 @@ header p { margin: 0; color: var(--amber-strong); font-size: 15px; }
|
|||||||
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
|
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
|
||||||
.row { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; margin-bottom: 12px; }
|
.row { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; margin-bottom: 12px; }
|
||||||
label { display: block; margin-bottom: 6px; font-weight: 600; font-size: 15px; }
|
label { display: block; margin-bottom: 6px; font-weight: 600; font-size: 15px; }
|
||||||
input[type="text"], select, textarea { width: 100%; border: 1px solid #cdd4e0; border-radius: 6px; padding: 10px 12px; font-size: 15px; background: #fff; }
|
input[type="text"], input[type="time"], select, textarea { width: 100%; border: 1px solid #cdd4e0; border-radius: 6px; padding: 10px 12px; font-size: 15px; background: #fff; }
|
||||||
textarea { min-height: 170px; resize: vertical; line-height: 1.45; }
|
textarea { min-height: 170px; resize: vertical; line-height: 1.45; }
|
||||||
|
.subject-prefix-control { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: stretch; }
|
||||||
|
.subject-prefix-control input { border-top-right-radius: 0; border-bottom-right-radius: 0; }
|
||||||
|
.subject-prefix-control span { display: inline-flex; align-items: center; border: 1px solid #cdd4e0; border-left: 0; border-radius: 0 6px 6px 0; background: #f8fafc; padding: 0 12px; color: var(--muted); white-space: nowrap; font-size: 15px; }
|
||||||
button, .btn { border: 1px solid #b7c6da; background: #fff; color: #1f2d3d; border-radius: 999px; padding: 8px 14px; cursor: pointer; text-decoration: none; font-size: 13px; line-height: 1; display: inline-flex; align-items: center; gap: 6px; }
|
button, .btn { border: 1px solid #b7c6da; background: #fff; color: #1f2d3d; border-radius: 999px; padding: 8px 14px; cursor: pointer; text-decoration: none; font-size: 13px; line-height: 1; display: inline-flex; align-items: center; gap: 6px; }
|
||||||
.btn.amber, button.primary { background: var(--amber); border-color: var(--amber); color: #fff; }
|
.btn.amber, button.primary { background: var(--amber); border-color: var(--amber); color: #fff; }
|
||||||
.btn.amber.active { background: var(--amber-strong); border-color: var(--amber-strong); }
|
.btn.amber.active { background: var(--amber-strong); border-color: var(--amber-strong); }
|
||||||
@@ -71,4 +74,4 @@ th { background: #f6f8fb; color: #4a5566; font-weight: 600; }
|
|||||||
.summary-card, .preview-box { border: 1px solid var(--border); background: #f8fafc; border-radius: 10px; padding: 12px; display: grid; gap: 8px; }
|
.summary-card, .preview-box { border: 1px solid var(--border); background: #f8fafc; border-radius: 10px; padding: 12px; display: grid; gap: 8px; }
|
||||||
.kv { display: grid; grid-template-columns: 180px 1fr; gap: 6px 10px; font-size: 15px; }
|
.kv { display: grid; grid-template-columns: 180px 1fr; gap: 6px 10px; font-size: 15px; }
|
||||||
.empty-state { min-height: 240px; display: grid; place-items: center; text-align: center; border: 1px dashed #cfd6e3; border-radius: 12px; background: #f8fafc; padding: 24px; }
|
.empty-state { min-height: 240px; display: grid; place-items: center; text-align: center; border: 1px dashed #cfd6e3; border-radius: 12px; background: #f8fafc; padding: 24px; }
|
||||||
@media (max-width: 760px) { header { display: block; } header h1 { font-size: 30px; margin-bottom: 4px; } .br-restore-layout { grid-template-columns: 1fr; } .top-actions { justify-content: flex-start; } .kv { grid-template-columns: 1fr; } }
|
@media (max-width: 760px) { header { display: block; } header h1 { font-size: 30px; margin-bottom: 4px; } .br-restore-layout { grid-template-columns: 1fr; } .top-actions { justify-content: flex-start; } .kv { grid-template-columns: 1fr; } .subject-prefix-control { grid-template-columns: 1fr; } .subject-prefix-control input { border-radius: 6px 6px 0 0; } .subject-prefix-control span { border-left: 1px solid #cdd4e0; border-top: 0; border-radius: 0 0 6px 6px; min-height: 38px; } }
|
||||||
|
|||||||
Reference in New Issue
Block a user