feat: use vacation subject prefix

This commit is contained in:
Marek Miklewicz
2026-06-02 21:28:31 +02:00
parent 64b62a5793
commit 56709817b5
18 changed files with 220 additions and 68 deletions
+40 -3
View File
@@ -5,7 +5,8 @@ function render_rule_form(AppContext $ctx, string $mode, ?array $rule): string
{
$isUpdate = $mode === 'update';
$id = $isUpdate ? (string)$rule['id'] : '';
$subject = $rule['subject'] ?? '';
$subjectPrefix = $rule['subject_prefix'] ?? $rule['subject'] ?? '';
$replyFrequency = selected_reply_frequency($rule);
$body = $rule['body'] ?? '';
$enabled = $rule === null || !empty($rule['enabled']);
$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) . '">' : '';
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>' .
'<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) .
@@ -25,6 +27,40 @@ function render_rule_form(AppContext $ctx, string $mode, ?array $rule): string
'</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> */
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
{
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
+1 -1
View File
@@ -13,7 +13,7 @@ return static function (AppContext $ctx): void {
$ctx->requireCsrf('delete:' . $id);
$ctx->rules->delete($ctx->daUser->username(), $id);
$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']));
} catch (Throwable $e) {
$ctx->render('Confirm deletion', '<div class="alert alert-err">' . AppContext::e($e->getMessage()) . '</div>');
+3 -2
View File
@@ -22,8 +22,9 @@ return static function (AppContext $ctx): void {
foreach ($rules as $rule) {
$id = (string)$rule['id'];
$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 .= '<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($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>';
@@ -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>' .
'<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);
};
+6 -1
View File
@@ -24,13 +24,18 @@ class DirectAdminVacationApi
[$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'] ?? '');
$replyFrequency = !empty($rule['reply_every_message']) ? '0' : (string)max(0, (int)($rule['repeat_minutes'] ?? 1440));
return [
'action' => $exists ? 'modify' : 'create',
'domain' => $domain,
'user' => $local,
'email' => $email,
'text' => (string)($rule['body'] ?? ''),
'subject' => (string)($rule['subject'] ?? ''),
'subject_prefix' => $subjectPrefix,
'subject' => $subjectPrefix,
'reply_frequency' => $replyFrequency,
'reply_once_time' => $replyFrequency,
'starttime' => $startTime,
'startyear' => $startYear,
'startmonth' => $startMonth,
+34 -9
View File
@@ -3,18 +3,27 @@ declare(strict_types=1);
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> */
public static function validate(array $input, Settings $settings): array
{
$subject = trim((string)($input['subject'] ?? ''));
if ($subject === '') {
throw new InvalidArgumentException('Temat jest wymagany.');
$subjectPrefix = trim((string)($input['subject_prefix'] ?? $input['subject'] ?? ''));
if (str_contains($subjectPrefix, "\r") || str_contains($subjectPrefix, "\n")) {
throw new InvalidArgumentException('Przedrostek tematu nie może zawierać nowych linii.');
}
if (str_contains($subject, "\r") || str_contains($subject, "\n")) {
throw new InvalidArgumentException('Temat nie może zawierać nowych linii.');
}
if (strlen($subject) > $settings->maxSubjectBytes()) {
throw new InvalidArgumentException('Temat jest za długi.');
if (strlen($subjectPrefix) > $settings->maxSubjectBytes()) {
throw new InvalidArgumentException('Przedrostek tematu jest za długi.');
}
$body = str_replace(["\r\n", "\r", "\0"], ["\n", "\n", ''], (string)($input['body'] ?? ''));
@@ -33,9 +42,10 @@ 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'));
return [
'subject' => $subject,
'subject_prefix' => $subjectPrefix,
'body' => $body,
'schedule_mode' => $mode,
'timezone' => $settings->timezone(),
@@ -44,10 +54,25 @@ final class RuleValidator
'start_ts' => $startTs,
'end_ts' => $endTs,
'enabled' => self::truthy($input['enabled'] ?? false),
'reply_every_message' => $replyEveryMessage,
'repeat_minutes' => $repeatMinutes,
'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} */
private static function parseScheduleValue(string $value, string $mode, DateTimeZone $tz, bool $start): array
{
-15
View File
@@ -52,8 +52,6 @@ final class Settings
'DEFAULT_ENABLE' => 'true',
'GLOBAL_AUTORESPONDER_TIMEZONE' => 'Europe/Warsaw',
'GLOBAL_AUTORESPONDER_BACKEND' => 'da',
'GLOBAL_AUTORESPONDER_REPLY_EVERY_MESSAGE' => 'false',
'GLOBAL_AUTORESPONDER_REPEAT_MINUTES' => '1440',
'GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE' => 'true',
'GLOBAL_AUTORESPONDER_MAX_SUBJECT_BYTES' => '255',
'GLOBAL_AUTORESPONDER_MAX_BODY_BYTES' => '20000',
@@ -103,16 +101,6 @@ final class Settings
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
{
return $this->getBool('GLOBAL_AUTORESPONDER_DYNAMIC_MAILBOX_SCOPE', true);
@@ -156,9 +144,6 @@ final class Settings
if (!in_array($this->backendMode(), ['da', 'exim'], true)) {
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) {
throw new InvalidArgumentException('Invalid size limits');
}