/ src / scripts / mailclone.js
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
(function () {
    "use strict";
    /*
     * wipe-mode confirmation: intercept the clone-form submit, if
     * the chosen mode is "wipe" prompt the user to type DELETE
     * before allowing the POST to proceed. The form's
     * data-confirm-wipe attribute carries the localized prompt
     * text.
     */
    var clone_form = document.querySelector(".mail-clone-form");
    if (clone_form) {
        clone_form.addEventListener("submit", function (event) {
            var mode = clone_form.querySelector(
                "input[name='clone_mode']:checked");
            if (!mode || mode.value !== "wipe") {
                return;
            }
            var prompt_text = clone_form.dataset.confirmWipe ||
                "Type DELETE to confirm wiping local mail.";
            var typed = window.prompt(prompt_text);
            if (typed !== "DELETE") {
                event.preventDefault();
            }
        });
    }
    /*
     * status-banner polling: every 5 seconds fetch the
     * userMailCloneStatus JSON for the active job and update the
     * in-DOM counters in place. Stops polling when the response
     * says active=false (job finished, cancelled, or failed). On
     * a terminal state, reload the page once so the banner
     * re-renders with the final shape (status message and
     * final-state UI without the cancel button).
     */
    var banner = document.querySelector(".mail-clone-status");
    if (!banner) {
        return;
    }
    var poll_url = banner.dataset.pollUrl;
    if (!poll_url) {
        return;
    }
    var POLL_INTERVAL_MS = 5000;
    var poll_timer = null;
    /* Clicking the Failed count (rendered as a link when there is
       at least one error) expands or collapses the full-width
       errors list below the status table. */
    var errors_toggle = banner.querySelector(
        ".mail-clone-failed-link");
    if (errors_toggle) {
        errors_toggle.addEventListener("click", function (event) {
            event.preventDefault();
            var errors_row = banner.querySelector(
                ".mail-clone-errors-row");
            if (errors_row) {
                errors_row.classList.toggle("none");
            }
        });
    }
    function update_banner(data) {
        if (!data) {
            poll_timer = window.setTimeout(poll, POLL_INTERVAL_MS);
            return;
        }
        /* Toggle the daemon-down warnings irrespective of
           whether a job is active: the user may sit on the
           status page after a clone completes and we still
           want them to see if MediaUpdater or MailServer is
           down so subsequent clones will not be silently
           blocked. */
        var updater_warn = document.querySelector(
            ".mail-clone-warn-updater");
        if (updater_warn) {
            updater_warn.classList.toggle("none",
                Boolean(data.media_updater_running));
        }
        var server_warn = document.querySelector(
            ".mail-clone-warn-server");
        if (server_warn) {
            server_warn.classList.toggle("none",
                Boolean(data.mail_server_running));
        }
        /* Show the Manage Machines link whenever either daemon is
           down so the path to start it stays one click away;
           hide it once both are running. */
        var manage_link = document.querySelector(
            ".mail-clone-manage-machines");
        if (manage_link) {
            manage_link.classList.toggle("none",
                Boolean(data.media_updater_running) &&
                Boolean(data.mail_server_running));
        }
        if (!data.active) {
            window.location.reload();
            return;
        }
        var folder = banner.querySelector(
            ".mail-clone-current-folder");
        if (folder) {
            folder.textContent = data.current_folder || "-";
        }
        var imported = banner.querySelector(
            ".mail-clone-imported");
        if (imported) {
            imported.textContent = data.imported;
        }
        var skipped = banner.querySelector(
            ".mail-clone-skipped");
        if (skipped) {
            skipped.textContent = data.skipped;
        }
        var failed_link = banner.querySelector(
            ".mail-clone-failed-link");
        if (failed_link) {
            failed_link.textContent = data.failed;
        }
        var failed_plain = banner.querySelector(
            ".mail-clone-failed-plain");
        if (failed_plain) {
            failed_plain.textContent = data.failed;
        }
        update_errors(data.errors || []);
        var status_text = banner.querySelector(
            ".mail-clone-status-text");
        if (status_text) {
            status_text.textContent = data.status;
        }
        poll_timer = window.setTimeout(poll, POLL_INTERVAL_MS);
    }
    /*
     * Renders the per-message error list beneath the Failed
     * counter. Each entry shows which UID and folder were
     * affected and the server's message; entries that later
     * succeeded a different way are shown struck-through with a
     * "later succeeded via X" note rather than being dropped, so
     * the user can see a transient failure was recovered instead
     * of being left wondering. The toggle link is only shown
     * when at least one error exists.
     */
    function update_errors(errors) {
        var failed_link = banner.querySelector(
            ".mail-clone-failed-link");
        var failed_plain = banner.querySelector(
            ".mail-clone-failed-plain");
        var container = banner.querySelector(
            ".mail-clone-errors");
        if (!container) {
            return;
        }
        var has_errors = errors.length > 0;
        /* When there is at least one error the count is shown as a
           clickable link that toggles the list; with zero errors
           the plain (non-link) count is shown and the list is
           force-collapsed. */
        if (failed_link) {
            failed_link.classList.toggle("none", !has_errors);
        }
        if (failed_plain) {
            failed_plain.classList.toggle("none", has_errors);
        }
        if (!has_errors) {
            var errors_row = banner.querySelector(
                ".mail-clone-errors-row");
            if (errors_row) {
                errors_row.classList.add("none");
            }
        }
        var template = container.dataset.resolvedViaTemplate ||
            "later succeeded via %s";
        var lines = [];
        var i = 0;
        for (i = 0; i < errors.length; i++) {
            lines.push(render_error_line(errors[i], template));
        }
        container.innerHTML = lines.join("");
    }
    /*
     * Builds one error <div> as an HTML string. textContent-style
     * escaping is done by hand on the dynamic fields because we
     * are assembling an innerHTML string; only the fixed template
     * markup is literal.
     */
    function render_error_line(error, template) {
        var where = [];
        if (error.uid) {
            where.push("UID " + error.uid);
        }
        if (error.folder) {
            where.push(escape_html(error.folder));
        }
        var prefix = where.length > 0 ?
            (where.join(", ") + ": ") : "";
        var body = escape_html(error.message || "");
        var resolved = Number(error.resolved_at) > 0;
        var note = "";
        if (resolved) {
            var via = escape_html(error.resolved_via || "");
            note = " <span class=\"mail-clone-error-resolved\">" +
                "(" + template.replace("%s", via) + ")</span>";
        }
        var css = resolved ? "mail-clone-error resolved" :
            "mail-clone-error";
        return "<div class=\"" + css + "\">" + prefix + body +
            note + "</div>";
    }
    /*
     * Minimal HTML-escaping for the dynamic error fields. Covers
     * the five characters that matter inside element text and
     * double-quoted attributes.
     */
    function escape_html(text) {
        return String(text)
            .replace(/&/g, "&")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#39;");
    }
    function poll() {
        fetch(poll_url, {credentials: "same-origin"})
            .then(function (response) {
                return response.json();
            })
            .then(update_banner)
            .catch(function () {
                /* transient network blip; retry next interval */
                poll_timer = window.setTimeout(poll,
                    POLL_INTERVAL_MS);
            });
    }
    poll_timer = window.setTimeout(poll, POLL_INTERVAL_MS);
})();
X