/**
* 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, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
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);
})();