Sync global-autoresponder current state
This commit is contained in:
@@ -1,186 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
BASE_DIR="/usr/local/hitme_plugins/global-autoresponders"
|
||||
STATE_FILE="$BASE_DIR/config/backend-state.json"
|
||||
PLUGIN_DIR="/usr/local/directadmin/plugins/global-autoresponder"
|
||||
CUSTOM_DIR="/usr/local/directadmin/custombuild/custom/exim"
|
||||
CUSTOM_CONF="$CUSTOM_DIR/exim.conf"
|
||||
SOURCE_CONF="/usr/local/directadmin/custombuild/configure/exim/exim.conf"
|
||||
ROUTER_SNIPPET="$PLUGIN_DIR/scripts/exim/global_autoresponder_router.conf"
|
||||
TRANSPORT_SNIPPET="$PLUGIN_DIR/scripts/exim/global_autoresponder_transport.conf"
|
||||
ROLLBACK_ACTIVE=0
|
||||
ROLLBACK_CREATED=0
|
||||
ROLLBACK_REBUILD=0
|
||||
ROLLBACK_FILE=
|
||||
|
||||
usage() {
|
||||
echo "Usage: scripts/backend.sh da|exim" >&2
|
||||
}
|
||||
|
||||
if [ "${1:-}" != "da" ] && [ "${1:-}" != "exim" ]; then
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$BASE_DIR/config" "$BASE_DIR/data" "$BASE_DIR/state" "$BASE_DIR/logs"
|
||||
chmod 700 "$BASE_DIR/config" "$BASE_DIR/logs"
|
||||
|
||||
write_state() {
|
||||
backend="$1"
|
||||
printf '{"active_backend":"%s","migration_status":"complete"}\n' "$backend" > "$STATE_FILE"
|
||||
chmod 600 "$STATE_FILE"
|
||||
}
|
||||
|
||||
remove_marked_block() {
|
||||
marker="$1"
|
||||
file="$2"
|
||||
tmp="${file}.global-autoresponder.$$"
|
||||
awk -v marker="$marker" '
|
||||
$0 == "# BEGIN global-autoresponder " marker { skip=1; next }
|
||||
$0 == "# END global-autoresponder " marker { skip=0; next }
|
||||
skip != 1 { print }
|
||||
' "$file" > "$tmp"
|
||||
mv "$tmp" "$file"
|
||||
}
|
||||
|
||||
insert_after_section() {
|
||||
section="$1"
|
||||
snippet="$2"
|
||||
file="$3"
|
||||
tmp="${file}.global-autoresponder.$$"
|
||||
if ! awk -v section="$section" -v snippet="$snippet" '
|
||||
BEGIN {
|
||||
while ((getline line < snippet) > 0) {
|
||||
block = block line "\n"
|
||||
}
|
||||
close(snippet)
|
||||
}
|
||||
{ print }
|
||||
$0 == section && inserted != 1 {
|
||||
printf "\n%s\n", block
|
||||
inserted = 1
|
||||
}
|
||||
END {
|
||||
if (inserted != 1) {
|
||||
exit 3
|
||||
}
|
||||
}
|
||||
' "$file" > "$tmp"; then
|
||||
rm -f "$tmp"
|
||||
echo "Cannot install exim backend: section $section not found in custom exim.conf." >&2
|
||||
exit 1
|
||||
fi
|
||||
mv "$tmp" "$file"
|
||||
}
|
||||
|
||||
backup_custom_conf() {
|
||||
mkdir -p "$CUSTOM_DIR"
|
||||
ROLLBACK_FILE="${CUSTOM_CONF}.global-autoresponder.backup.$$"
|
||||
if [ -f "$CUSTOM_CONF" ]; then
|
||||
cp "$CUSTOM_CONF" "$ROLLBACK_FILE"
|
||||
ROLLBACK_CREATED=0
|
||||
else
|
||||
: > "$ROLLBACK_FILE"
|
||||
ROLLBACK_CREATED=1
|
||||
fi
|
||||
ROLLBACK_ACTIVE=1
|
||||
}
|
||||
|
||||
restore_custom_conf() {
|
||||
if [ "$ROLLBACK_ACTIVE" != "1" ]; then
|
||||
return
|
||||
fi
|
||||
set +e
|
||||
if [ "$ROLLBACK_CREATED" = "1" ]; then
|
||||
rm -f "$CUSTOM_CONF"
|
||||
else
|
||||
cp "$ROLLBACK_FILE" "$CUSTOM_CONF"
|
||||
fi
|
||||
rm -f "$ROLLBACK_FILE"
|
||||
if [ "$ROLLBACK_REBUILD" = "1" ]; then
|
||||
rebuild_and_reload_exim >/dev/null 2>&1
|
||||
fi
|
||||
ROLLBACK_ACTIVE=0
|
||||
}
|
||||
|
||||
commit_custom_conf() {
|
||||
rm -f "$ROLLBACK_FILE"
|
||||
ROLLBACK_ACTIVE=0
|
||||
ROLLBACK_CREATED=0
|
||||
ROLLBACK_REBUILD=0
|
||||
}
|
||||
|
||||
trap 'restore_custom_conf' EXIT
|
||||
trap 'restore_custom_conf; exit 1' HUP INT TERM
|
||||
|
||||
validate_exim_conf() {
|
||||
if command -v exim >/dev/null 2>&1; then
|
||||
exim -C "$CUSTOM_CONF" -bV >/dev/null
|
||||
elif command -v exim4 >/dev/null 2>&1; then
|
||||
exim4 -C "$CUSTOM_CONF" -bV >/dev/null
|
||||
else
|
||||
echo "Cannot validate exim backend: exim binary not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
rebuild_and_reload_exim() {
|
||||
if command -v da >/dev/null 2>&1; then
|
||||
da build exim_conf
|
||||
elif [ -x /usr/local/directadmin/custombuild/build ]; then
|
||||
/usr/local/directadmin/custombuild/build exim_conf
|
||||
else
|
||||
echo "Cannot rebuild exim configuration: da/build command not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl reload exim 2>/dev/null || systemctl restart exim
|
||||
else
|
||||
service exim reload 2>/dev/null || service exim restart
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$1" = "da" ]; then
|
||||
if [ -f "$CUSTOM_CONF" ]; then
|
||||
backup_custom_conf
|
||||
remove_marked_block "router" "$CUSTOM_CONF"
|
||||
remove_marked_block "transport" "$CUSTOM_CONF"
|
||||
if grep -Fq "global_autoresponder_" "$CUSTOM_CONF"; then
|
||||
echo "Cannot switch to da: plugin Exim markers were not removed cleanly." >&2
|
||||
exit 1
|
||||
fi
|
||||
validate_exim_conf
|
||||
ROLLBACK_REBUILD=1
|
||||
rebuild_and_reload_exim
|
||||
commit_custom_conf
|
||||
fi
|
||||
write_state "da"
|
||||
echo "Backend set to da"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -d "$(dirname "$SOURCE_CONF")" ]; then
|
||||
echo "Cannot enable exim backend: DirectAdmin CustomBuild exim source config not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$ROUTER_SNIPPET" ] || [ ! -f "$TRANSPORT_SNIPPET" ]; then
|
||||
echo "Cannot enable exim backend: plugin Exim snippets are missing." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
backup_custom_conf
|
||||
if [ ! -f "$CUSTOM_CONF" ]; then
|
||||
cp "$SOURCE_CONF" "$CUSTOM_CONF"
|
||||
fi
|
||||
|
||||
remove_marked_block "router" "$CUSTOM_CONF"
|
||||
remove_marked_block "transport" "$CUSTOM_CONF"
|
||||
insert_after_section "begin routers" "$ROUTER_SNIPPET" "$CUSTOM_CONF"
|
||||
insert_after_section "begin transports" "$TRANSPORT_SNIPPET" "$CUSTOM_CONF"
|
||||
validate_exim_conf
|
||||
ROLLBACK_REBUILD=1
|
||||
rebuild_and_reload_exim
|
||||
commit_custom_conf
|
||||
write_state "exim"
|
||||
echo "Backend set to exim"
|
||||
@@ -1,11 +0,0 @@
|
||||
# BEGIN global-autoresponder router
|
||||
# Runs an unseen delivery to the plugin worker for local recipients. Normal mailbox
|
||||
# delivery continues because unseen is enabled and the worker suppresses ineligible mail.
|
||||
global_autoresponder_router:
|
||||
driver = accept
|
||||
domains = +local_domains
|
||||
condition = ${if exists{/usr/local/directadmin/plugins/global-autoresponder/scripts/global_autoresponder_worker.php}{yes}{no}}
|
||||
transport = global_autoresponder_transport
|
||||
unseen
|
||||
no_verify
|
||||
# END global-autoresponder router
|
||||
@@ -1,13 +0,0 @@
|
||||
# BEGIN global-autoresponder transport
|
||||
# The worker receives the original message on stdin and Exim-provided envelope
|
||||
# variables such as SENDER, RECIPIENT, LOCAL_PART, and DOMAIN in the environment.
|
||||
global_autoresponder_transport:
|
||||
driver = pipe
|
||||
command = /usr/local/directadmin/plugins/global-autoresponder/scripts/global_autoresponder_worker.php
|
||||
user = root
|
||||
group = root
|
||||
return_fail_output = false
|
||||
log_output = true
|
||||
message_prefix =
|
||||
message_suffix =
|
||||
# END global-autoresponder transport
|
||||
@@ -1,199 +0,0 @@
|
||||
#!/usr/local/bin/php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__) . '/exec/lib/Settings.php';
|
||||
require_once dirname(__DIR__) . '/exec/lib/MessageClassifier.php';
|
||||
require_once dirname(__DIR__) . '/exec/lib/RepeatState.php';
|
||||
require_once dirname(__DIR__) . '/exec/lib/MailSender.php';
|
||||
require_once dirname(__DIR__) . '/exec/lib/MailboxDirectory.php';
|
||||
require_once dirname(__DIR__) . '/exec/lib/RuleRepository.php';
|
||||
|
||||
final class GlobalAutoresponderWorker
|
||||
{
|
||||
/** @param callable(string,string,string,string):bool $send */
|
||||
public function __construct(
|
||||
private Settings $settings,
|
||||
private RuleRepository $rules,
|
||||
private MailboxDirectory $mailboxes,
|
||||
private RepeatState $repeat,
|
||||
private $send,
|
||||
private ?int $now = null
|
||||
) {
|
||||
}
|
||||
|
||||
/** @param array<string,string> $env */
|
||||
public function handle(array $env, string $message): int
|
||||
{
|
||||
if ($this->settings->backendMode() !== 'exim') {
|
||||
return 0;
|
||||
}
|
||||
$sender = self::firstEnv($env, ['SENDER_ADDRESS', 'sender_address', 'SENDER', 'sender']);
|
||||
$recipient = self::recipientFromEnv($env);
|
||||
if ($sender === '' || $recipient === '' || !self::isEmail($sender) || !self::isEmail($recipient)) {
|
||||
return 0;
|
||||
}
|
||||
$headers = self::parseHeaders($message);
|
||||
if (MessageClassifier::shouldSuppress($headers, $sender)) {
|
||||
return 0;
|
||||
}
|
||||
$resolved = $this->mailboxes->resolveRecipient($recipient);
|
||||
if ($resolved === null) {
|
||||
return 0;
|
||||
}
|
||||
$sent = 0;
|
||||
$owner = $resolved['owner'];
|
||||
$recipient = $resolved['email'];
|
||||
foreach ($this->rules->all($owner) as $rule) {
|
||||
if (!$this->isActiveRule($rule)) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->ruleIncludesRecipient($rule, $recipient)) {
|
||||
continue;
|
||||
}
|
||||
$ruleId = (string)($rule['id'] ?? '');
|
||||
if ($ruleId === '') {
|
||||
continue;
|
||||
}
|
||||
$repeatMinutes = !empty($rule['reply_every_message']) ? 0 : max(0, (int)($rule['repeat_minutes'] ?? 1440));
|
||||
if (!$this->repeat->shouldSend($owner, $ruleId, $recipient, $sender, $repeatMinutes)) {
|
||||
continue;
|
||||
}
|
||||
$replySubject = self::replySubject(
|
||||
(string)($rule['subject_prefix'] ?? $rule['subject'] ?? ''),
|
||||
self::headerValue($headers, 'subject')
|
||||
);
|
||||
$ok = (bool)call_user_func(
|
||||
$this->send,
|
||||
$recipient,
|
||||
strtolower($sender),
|
||||
$replySubject,
|
||||
(string)$rule['body']
|
||||
);
|
||||
if ($ok) {
|
||||
$this->repeat->record($owner, $ruleId, $recipient, $sender);
|
||||
$sent++;
|
||||
}
|
||||
}
|
||||
return $sent;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $rule */
|
||||
private function isActiveRule(array $rule): bool
|
||||
{
|
||||
if (empty($rule['enabled'])) {
|
||||
return false;
|
||||
}
|
||||
$now = $this->now ?? time();
|
||||
return (int)($rule['start_ts'] ?? 0) <= $now && (int)($rule['end_ts'] ?? 0) >= $now;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $rule */
|
||||
private function ruleIncludesRecipient(array $rule, string $recipient): bool
|
||||
{
|
||||
if (!array_key_exists('dynamic_scope', $rule) || !empty($rule['dynamic_scope'])) {
|
||||
return true;
|
||||
}
|
||||
$snapshot = $rule['mailbox_snapshot'] ?? [];
|
||||
if (!is_array($snapshot)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($snapshot as $mailbox) {
|
||||
if (is_array($mailbox) && strtolower((string)($mailbox['email'] ?? '')) === strtolower($recipient)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param array<string,string> $env @param string[] $keys */
|
||||
private static function firstEnv(array $env, array $keys): string
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
$value = trim((string)($env[$key] ?? ''));
|
||||
if ($value !== '') {
|
||||
return strtolower($value);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** @param array<string,string> $env */
|
||||
private static function recipientFromEnv(array $env): string
|
||||
{
|
||||
$direct = self::firstEnv($env, ['LOCAL_PART_DOMAIN', 'RECIPIENT', 'ORIGINAL_RECIPIENT', 'local_part_domain']);
|
||||
if ($direct !== '') {
|
||||
return $direct;
|
||||
}
|
||||
$local = self::firstEnv($env, ['LOCAL_PART', 'local_part']);
|
||||
$domain = self::firstEnv($env, ['DOMAIN', 'domain']);
|
||||
return ($local !== '' && $domain !== '') ? $local . '@' . $domain : '';
|
||||
}
|
||||
|
||||
private static function isEmail(string $value): bool
|
||||
{
|
||||
return (bool)preg_match('/^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+$/', $value);
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
public static function parseHeaders(string $message): array
|
||||
{
|
||||
$headers = [];
|
||||
$current = '';
|
||||
foreach (preg_split('/\r\n|\n|\r/', $message) ?: [] as $line) {
|
||||
if ($line === '') {
|
||||
break;
|
||||
}
|
||||
if ($current !== '' && preg_match('/^[ \t]/', $line)) {
|
||||
$headers[$current] .= ' ' . trim($line);
|
||||
continue;
|
||||
}
|
||||
if (!str_contains($line, ':')) {
|
||||
continue;
|
||||
}
|
||||
[$key, $value] = explode(':', $line, 2);
|
||||
$current = trim($key);
|
||||
if ($current !== '') {
|
||||
$headers[$current] = trim($value);
|
||||
}
|
||||
}
|
||||
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__) {
|
||||
$settings = Settings::load(Settings::EXTERNAL_SETTINGS);
|
||||
$sender = new MailSender();
|
||||
$worker = new GlobalAutoresponderWorker(
|
||||
$settings,
|
||||
new RuleRepository(),
|
||||
new MailboxDirectory(),
|
||||
new RepeatState(),
|
||||
static fn (string $from, string $to, string $subject, string $body): bool => $sender->send('/usr/sbin/sendmail', $from, $to, $subject, $body)
|
||||
);
|
||||
$worker->handle(getenv(), stream_get_contents(STDIN) ?: '');
|
||||
}
|
||||
+2
-7
@@ -5,20 +5,15 @@ BASE_DIR="/usr/local/hitme_plugins/global-autoresponders"
|
||||
PLUGIN_DIR="/usr/local/directadmin/plugins/global-autoresponder"
|
||||
CRON_FILE="/etc/cron.d/global-autoresponder"
|
||||
|
||||
mkdir -p "$BASE_DIR/config/users" "$BASE_DIR/data/users" "$BASE_DIR/state/replies" "$BASE_DIR/logs"
|
||||
mkdir -p "$BASE_DIR/config/users" "$BASE_DIR/data/users" "$BASE_DIR/logs"
|
||||
chmod 700 "$BASE_DIR/config" "$BASE_DIR/logs"
|
||||
chmod 711 "$BASE_DIR/data" "$BASE_DIR/state"
|
||||
chmod 711 "$BASE_DIR/data"
|
||||
|
||||
if [ ! -f "$BASE_DIR/config/plugin-settings.conf" ]; then
|
||||
cp "$PLUGIN_DIR/plugin-settings.conf" "$BASE_DIR/config/plugin-settings.conf"
|
||||
chmod 600 "$BASE_DIR/config/plugin-settings.conf"
|
||||
fi
|
||||
|
||||
if [ ! -f "$BASE_DIR/config/backend-state.json" ]; then
|
||||
printf '%s\n' '{"active_backend":"da","migration_status":"complete"}' > "$BASE_DIR/config/backend-state.json"
|
||||
chmod 600 "$BASE_DIR/config/backend-state.json"
|
||||
fi
|
||||
|
||||
touch "$BASE_DIR/logs/audit.log"
|
||||
chmod 600 "$BASE_DIR/logs/audit.log"
|
||||
|
||||
|
||||
@@ -38,11 +38,7 @@ try {
|
||||
throw new InvalidArgumentException('Invalid username');
|
||||
}
|
||||
|
||||
$settings = Settings::load(Settings::EXTERNAL_SETTINGS);
|
||||
if ($settings->backendMode() !== 'da') {
|
||||
echo "DirectAdmin backend is not active; nothing to sync.\n";
|
||||
exit(0);
|
||||
}
|
||||
Settings::load(Settings::EXTERNAL_SETTINGS);
|
||||
|
||||
$users = $targetUser !== '' ? [$targetUser] : users_with_rules(Settings::DATA_DIR);
|
||||
$service = new DirectAdminVacationSyncService(
|
||||
|
||||
@@ -2,12 +2,8 @@
|
||||
set -eu
|
||||
|
||||
BASE_DIR="/usr/local/hitme_plugins/global-autoresponders"
|
||||
PLUGIN_DIR="/usr/local/directadmin/plugins/global-autoresponder"
|
||||
CRON_FILE="/etc/cron.d/global-autoresponder"
|
||||
|
||||
if [ -x "$PLUGIN_DIR/scripts/backend.sh" ]; then
|
||||
"$PLUGIN_DIR/scripts/backend.sh" da || true
|
||||
fi
|
||||
rm -f "$CRON_FILE"
|
||||
|
||||
if [ "${PURGE_GLOBAL_AUTORESPONDER_DATA:-0}" = "1" ]; then
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ set -eu
|
||||
BASE_DIR="/usr/local/hitme_plugins/global-autoresponders"
|
||||
PLUGIN_DIR="/usr/local/directadmin/plugins/global-autoresponder"
|
||||
CRON_FILE="/etc/cron.d/global-autoresponder"
|
||||
mkdir -p "$BASE_DIR/config/users" "$BASE_DIR/data/users" "$BASE_DIR/state/replies" "$BASE_DIR/logs"
|
||||
mkdir -p "$BASE_DIR/config/users" "$BASE_DIR/data/users" "$BASE_DIR/logs"
|
||||
printf '%s\n' "*/15 * * * * root $PLUGIN_DIR/scripts/sync_directadmin_vacations.php >/dev/null 2>&1" > "$CRON_FILE"
|
||||
chmod 644 "$CRON_FILE"
|
||||
echo "global-autoresponder updated; external configuration preserved"
|
||||
|
||||
Reference in New Issue
Block a user