Manage Usermin Forwarding and auto reply using the remote api

SYSTEM INFORMATION
DEBIAN 12 REQUIRED
8.1.0 REQUIRED
2.5.6 REQUIRED

How can you manage the usermin settings like forwarding, auto clearing, auto reply. as the auto reply in virtualmin is differnet to the usermin auto reply via the remote api command

When there are a large number of mailboxes in a domain / virtual server, this can become tedious to do manually. I wish there was a select all mailboxes for things like away messages during holidays.

This is the php code for an internal tool which can be used to apply an away message autoresponder to multiple (all) mailboxes of a domain / virtual server. It has worked when it was last used many versions of Virtualmin ago

<?php
session_start();

// ==========================================
// CONFIGURATION
// ==========================================
$vm_host = 'https://vps16.indiax.com:10000'; // Replace with your Virtualmin IP / URL
$root_user = 'root';
$root_pass = 'dfgdfgdfgdfgdfg';    // Replace with your root password

// Google reCAPTCHA v2 Keys
$recaptcha_site_key = 'dfgdfgdfgdfgdfg';     // Replace with your Site Key
$recaptcha_secret_key = 'dfgdfgdfgdfgdfg'; // Replace with your Secret Key

$max_attempts = 10;
$rate_limit_file = __DIR__ . '/rate_limit.json';

// ==========================================
// HELPER FUNCTIONS
// ==========================================
function call_virtualmin($program, $params, $user, $pass) {
    global $vm_host;
    $url = $vm_host . '/virtual-server/remote.cgi?program=' . $program;
    foreach ($params as $k => $v) {
        $url .= ($v === null) ? "&$k" : "&$k=" . urlencode($v);
    }
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_USERPWD, "$user:$pass");
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30); 
    
    $response = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    return ['status' => $status, 'body' => $response];
}

function verify_recaptcha($secret, $response) {
    $url = 'https://www.google.com/recaptcha/api/siteverify';
    $data = ['secret' => $secret, 'response' => $response];
    
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    $result = curl_exec($ch);
    curl_close($ch);
    
    $json = json_decode($result, true);
    return isset($json['success']) && $json['success'] === true;
}

function check_rate_limit($ip) {
    global $rate_limit_file, $max_attempts;
    $data = file_exists($rate_limit_file) ? json_decode(file_get_contents($rate_limit_file), true) : [];
    $today = date('Y-m-d');
    
    foreach($data as $k => $v) {
        if ($v['date'] !== $today) unset($data[$k]);
    }

    if (!isset($data[$ip])) {
        $data[$ip] = ['date' => $today, 'count' => 0];
    }
    if ($data[$ip]['count'] >= $max_attempts) return false;
    
    $data[$ip]['count']++;
    file_put_contents($rate_limit_file, json_encode($data));
    return true;
}

// ==========================================
// AJAX API HANDLERS
// ==========================================
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax_action'])) {
    header('Content-Type: application/json');
    
    // --- ACTION: INITIALIZE & GET MAILBOXES ---
    if ($_POST['ajax_action'] === 'init') {
        $client_ip = $_SERVER['REMOTE_ADDR'];
        $domain = trim($_POST['domain']);
        $client_user = trim($_POST['client_user']);
        $client_pass = $_POST['client_pass'];
        
        // 1. Verify Google reCAPTCHA
        if (empty($_POST['g-recaptcha-response'])) {
            echo json_encode(['success' => false, 'error' => 'Please complete the CAPTCHA.']); exit;
        }
        if (!verify_recaptcha($recaptcha_secret_key, $_POST['g-recaptcha-response'])) {
            echo json_encode(['success' => false, 'error' => 'CAPTCHA verification failed. Please try again.']); exit;
        }
        
        // 2. Verify Rate Limit
        if (!check_rate_limit($client_ip)) {
            echo json_encode(['success' => false, 'error' => "Rate limit exceeded ($max_attempts/day)."]); exit;
        }

        // 3. Authenticate Client Credentials
        $auth_test = call_virtualmin('info', [], $client_user, $client_pass);
        if ($auth_test['status'] === 401) {
            echo json_encode(['success' => false, 'error' => 'Invalid client username or password.']); exit;
        }

        // 4. Check Domain Ownership via Root
        $root_domains_res = call_virtualmin('list-domains', ['json' => 1], $root_user, $root_pass);
        $domains_data = json_decode($root_domains_res['body'], true);
        
        $owns_domain = false;
        if(isset($domains_data['data'])) {
            foreach($domains_data['data'] as $d) {
                $raw_name = trim($d['name']);
                $parts = preg_split('/\s+/', $raw_name);
                $d_domain = isset($parts[0]) ? trim(strtolower($parts[0])) : '';
                $d_owner = isset($parts[1]) ? trim(strtolower($parts[1])) : '';
                
                if (empty($d_owner) && isset($d['values']['username'][0])) $d_owner = trim(strtolower($d['values']['username'][0]));
                if (empty($d_owner) && isset($d['username'])) $d_owner = trim(strtolower($d['username']));

                if ($d_domain === trim(strtolower($domain)) && $d_owner === trim(strtolower($client_user))) {
                    $owns_domain = true; break; 
                }
            }
        }

        if (!$owns_domain) {
            echo json_encode(['success' => false, 'error' => "Domain not found or not owned by '$client_user'."]); exit;
        }

        // 5. Fetch Mailboxes via Root
        $users_res = call_virtualmin('list-users', ['domain' => $domain, 'json' => 1], $root_user, $root_pass);
        $users_data = json_decode($users_res['body'], true);
        
        $valid_mailboxes = [];
        if (isset($users_data['data'])) {
            foreach ($users_data['data'] as $u) {
                $raw_string = trim($u['name']);
                if (stripos($raw_string, 'User Real name') !== false || strpos($raw_string, '---') === 0) continue;
                $parts = preg_split('/\s+/', $raw_string);
                if (!empty($parts[0])) {
                    $valid_mailboxes[] = $parts[0];
                }
            }
        }

        if (empty($valid_mailboxes)) {
            echo json_encode(['success' => false, 'error' => 'No mailboxes found for this domain.']); exit;
        }

        $_SESSION['authorized_domain'] = trim(strtolower($domain));
        echo json_encode(['success' => true, 'mailboxes' => $valid_mailboxes]);
        exit;
    }

    // --- ACTION: UPDATE SINGLE USER ---
    if ($_POST['ajax_action'] === 'update_user') {
        $domain = trim($_POST['domain']);
        $username = trim($_POST['user']);
        $action = $_POST['action'];
        $autoreply_msg = trim($_POST['autoreply_msg']);

        if (!isset($_SESSION['authorized_domain']) || $_SESSION['authorized_domain'] !== strtolower($domain)) {
            echo json_encode(['success' => false, 'message' => 'Unauthorized domain operation.']); exit;
        }

        $params = ['domain' => $domain, 'user' => $username];
        if ($action === 'enable') {
            $params['autoreply'] = $autoreply_msg;
        } else {
            $params['no-autoreply'] = null; 
        }
        
        $mod_res = call_virtualmin('modify-user', $params, $root_user, $root_pass);
        
        $body_lower = strtolower($mod_res['body']);
        if ($mod_res['status'] === 200 && strpos($body_lower, 'failed') === false && strpos($body_lower, 'error') === false) {
            echo json_encode(['success' => true, 'message' => 'Success']);
        } else {
            $error_snippet = htmlspecialchars(strip_tags(substr(trim($mod_res['body']), 0, 80)));
            echo json_encode(['success' => false, 'message' => "Failed ($error_snippet)"]);
        }
        exit;
    }
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Virtualmin Bulk Autoresponder</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
<body class="bg-light pb-5">

<div class="container mt-5" style="max-width: 600px;">
    
    <div class="card shadow-sm" id="formCard">
        <div class="card-header bg-primary text-white">
            <h4 class="mb-0">Bulk Autoresponder Management</h4>
        </div>
        <div class="card-body">
            <div id="errorBox" class="alert alert-danger" style="display:none;"></div>

            <form id="autoresponderForm" onsubmit="startProcess(event)">
                <div class="mb-3">
                    <label class="form-label">Domain Name</label>
                    <input type="text" id="domain" class="form-control" placeholder="example.com" required>
                </div>
                
                <div class="row mb-3">
                    <div class="col-md-6">
                        <label class="form-label">Domain Owner Username</label>
                        <input type="text" id="client_user" class="form-control" required>
                    </div>
                    <div class="col-md-6">
                        <label class="form-label">Domain Owner Password</label>
                        <input type="password" id="client_pass" class="form-control" required>
                    </div>
                </div>

                <div class="mb-3">
                    <label class="form-label">Action</label>
                    <select id="actionSelect" class="form-select" required>
                        <option value="enable">Enable Autoresponder for ALL mailboxes</option>
                        <option value="disable">Disable Autoresponder for ALL mailboxes</option>
                    </select>
                </div>

                <div class="mb-3" id="msgBox">
                    <label class="form-label">Autoresponder Message</label>
                    <textarea id="autoreply_msg" class="form-control" rows="4"></textarea>
                </div>

                <div class="mb-4 d-flex justify-content-center">
                    <div class="g-recaptcha" data-sitekey="<?= htmlspecialchars($recaptcha_site_key) ?>"></div>
                </div>

                <button type="submit" id="submitBtn" class="btn btn-primary w-100 fw-bold">Execute Bulk Action</button>
            </form>
        </div>
    </div>

    <div class="card mt-4 shadow-sm" id="outputCard" style="display:none;">
        <div class="card-header bg-dark text-white d-flex justify-content-between">
            <h5 class="mb-0">API Execution Output</h5>
            <small class="text-warning">Real-time processing</small>
        </div>
        <div class="card-body bg-black text-light" id="terminal" style="font-family: monospace; height: 500px; overflow-y: auto;">
            </div>
        <div class="card-footer bg-dark border-0">
             <button onclick="location.reload()" class="btn btn-sm btn-outline-light" id="restartBtn" style="display:none;">Start Over</button>
        </div>
    </div>

</div>

<script>
    // Toggle auto-reply message box
    document.getElementById('actionSelect').addEventListener('change', function() {
        document.getElementById('msgBox').style.display = (this.value === 'disable') ? 'none' : 'block';
    });

    const terminal = document.getElementById('terminal');
    function logMsg(msg) {
        terminal.innerHTML += msg + "<br>";
        terminal.scrollTop = terminal.scrollHeight;
    }

    async function startProcess(e) {
        e.preventDefault();
        
        const btn = document.getElementById('submitBtn');
        const errorBox = document.getElementById('errorBox');
        
        // Form inputs
        const domain = document.getElementById('domain').value;
        const client_user = document.getElementById('client_user').value;
        const client_pass = document.getElementById('client_pass').value;
        const action = document.getElementById('actionSelect').value;
        const autoreply_msg = document.getElementById('autoreply_msg').value;
        
        // Grab reCAPTCHA response
        const recaptchaResponse = grecaptcha.getResponse();
        
        if (recaptchaResponse.length === 0) {
            errorBox.innerText = 'Please check the "I\'m not a robot" box.';
            errorBox.style.display = 'block';
            return;
        }

        // UI Reset
        errorBox.style.display = 'none';
        btn.disabled = true;
        btn.innerText = 'Authenticating...';

        try {
            // STEP 1: INITIALIZE & GET USERS
            const formData = new URLSearchParams();
            formData.append('ajax_action', 'init');
            formData.append('domain', domain);
            formData.append('client_user', client_user);
            formData.append('client_pass', client_pass);
            formData.append('g-recaptcha-response', recaptchaResponse);

            let response = await fetch(window.location.href, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: formData.toString()
            });
            let result = await response.json();

            if (!result.success) {
                errorBox.innerText = result.error;
                errorBox.style.display = 'block';
                btn.disabled = false;
                btn.innerText = 'Execute Bulk Action';
                grecaptcha.reset(); // Reset the captcha so they can try again
                return;
            }

            // Authentication passed! Switch UI to terminal
            document.getElementById('formCard').style.display = 'none';
            document.getElementById('outputCard').style.display = 'block';
            
            const mailboxes = result.mailboxes;
            logMsg("<span class='text-success'>Authentication successful. Domain verified.</span>");
            logMsg(`Found <strong>${mailboxes.length}</strong> mailboxes. Starting bulk update...<br>`);

            // STEP 2: LOOP AND UPDATE EACH USER
            for (let i = 0; i < mailboxes.length; i++) {
                let user = mailboxes[i];
                logMsg(`Updating mailbox <span class='text-info'>${user}</span> ... <span id="status-${i}" class="text-warning">Processing</span>`);
                
                const updateData = new URLSearchParams();
                updateData.append('ajax_action', 'update_user');
                updateData.append('domain', domain);
                updateData.append('user', user);
                updateData.append('action', action);
                updateData.append('autoreply_msg', autoreply_msg);

                let updateReq = await fetch(window.location.href, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: updateData.toString()
                });
                
                let updateRes = await updateReq.json();
                const statusSpan = document.getElementById(`status-${i}`);
                
                if (updateRes.success) {
                    statusSpan.className = 'text-success';
                    statusSpan.innerText = 'Success';
                } else {
                    statusSpan.className = 'text-danger';
                    statusSpan.innerText = updateRes.message;
                }
            }

            logMsg("<br><span class='text-success fw-bold'>Bulk operation completed successfully.</span>");
            document.getElementById('restartBtn').style.display = 'inline-block';

        } catch (error) {
            logMsg(`<br><span class='text-danger'>A network or server error occurred: ${error.message}</span>`);
            document.getElementById('restartBtn').style.display = 'inline-block';
        }
    }
</script>
</body>
</html>

This only modifies the website/virtualmin auto reply and not the topic usermin autoreply

The code could be modified as required.