Files
imapsync/html/imapsync_form.js

849 lines
25 KiB
JavaScript

// $Id: proximapsync_form.js,v 1.15 2025/10/18 14:16:20 gilles Exp gilles $
/*jslint browser: true*/ /*global $*/
/*
1) How the hell is structured this code?
2) What is its behavior?
*/
var readyStateStr = {
"0": "Request not initialized",
"1": "Server connection established",
"2": "Response headers received",
"3": "Processing request",
"4": "Finished and response is ready"
};
var refresh_interval_ms = 2000;
var refresh_interval_s = refresh_interval_ms / 1000;
var test = {
counter_all: 0,
counter_ok: 0,
counter_nok: 0,
failed_tests: ""
};
var is = function is(expected, given, comment) {
test.counter_all += 1;
var message = test.counter_all + " - ["
+ expected
+ "] === ["
+ given
+ "] "
+ comment
+ "\n";
if (expected === given) {
test.counter_ok += 1;
message = "ok " + message;
}
else {
test.counter_nok += 1;
test.failed_tests += "nb " + message + "\n";
message = "not ok " + message;
}
$("#tests").append(message);
};
var note = function note(message) {
$("#tests").append(message);
};
var tests_last_x_lines = function tests_last_x_lines() {
is("", last_x_lines(), "last_x_lines: no args => empty string");
is("", last_x_lines(""), "last_x_lines: empty string => empty string");
is("abc", last_x_lines("abc"), "last_x_lines: abc => abc");
is("abc\ndef", last_x_lines("abc\ndef"), "last_x_lines: abc\ndef => abc\ndef");
is("def", last_x_lines("abc\ndef", -1), "last_x_lines: abc\ndef -1 => def\n");
is("", last_x_lines("abc\ndef", 0), "last_x_lines: abc\ndef 0 => empty string");
is("abc\ndef", last_x_lines("abc\ndef", -10), "last_x_lines: last 10 of 2 lines => 2 lines");
is("4\n5\n", last_x_lines("1\n2\n3\n4\n5\n", -3), "last_x_lines: last 3 lines of 5 lines");
is("3\n4\n5", last_x_lines("1\n2\n3\n4\n5", -3), "last_x_lines: last 3 lines of 5 lines");
};
var last_x_lines = function last_x_lines(string, num) {
if (undefined === string || 0 === num) {
return "";
}
return string.split(/\r?\n/).slice(num).join("\n");
};
var last_eta = function last_eta(string) {
// return the last occurrence of the substring "ETA: ...\n"
// or "ETA: unknown" or ""
var eta;
var last_found;
if (undefined === string) {
return "";
}
var eta_re = /ETA:.*\n/g;
eta = string.match(eta_re);
if (eta) {
last_found = eta[eta.length - 1];
return last_found;
}
else {
return "ETA: unknown";
}
}
var tests_last_eta = function tests_last_eta() {
is("", last_eta(), "last_eta: no args => empty string");
is(
"ETA: unknown",
last_eta(""),
"last_eta: empty => empty string");
is("ETA: unknown",
last_eta("ETA"),
"last_eta: ETA => empty string");
is("ETA: unknown", last_eta("ETA: but no CR"),
"last_eta: ETA: but no CR => empty string");
is(
"ETA: with CR\n",
last_eta("Blabla ETA: with CR\n"),
"last_eta: ETA: with CR => ETA: with CR"
);
is(
"ETA: 2 with CR\n",
last_eta("Blabla ETA: 1 with CR\nBlabla ETA: 2 with CR\n"),
"last_eta: several ETA: with CR => ETA: 2 with CR"
);
}
var tests_decompose_eta_line = function tests_decompose_eta_line() {
var eta_obj;
var eta_str = "ETA: Wed Jul 3 14:55:27 2019 1234 s 123/4567 msgs left\n";
eta_obj = decompose_eta_line("");
is(
"",
eta_obj.str,
"decompose_eta_line: no match => undefined"
);
eta_obj = decompose_eta_line(eta_str);
is(
eta_str,
eta_str,
"decompose_eta_line: str is str"
);
is(
eta_str,
eta_obj.str,
"decompose_eta_line: str back"
);
is(
"Wed Jul 3 14:55:27 2019",
eta_obj.date,
"decompose_eta_line: date"
);
is(
"1234",
eta_obj.seconds_left,
"decompose_eta_line: seconds_left"
);
is(
"123",
eta_obj.msgs_left,
"decompose_eta_line: msgs_left"
);
is(
"4567",
eta_obj.msgs_total,
"decompose_eta_line: msgs_total"
);
is(
"4444",
eta_obj.msgs_done(),
"decompose_eta_line: msgs_done"
);
is(
"97.31",
eta_obj.percent_done(),
"decompose_eta_line: percent_done"
);
is(
"2.69",
eta_obj.percent_left(),
"decompose_eta_line: percent_left"
);
};
var decompose_eta_line = function decompose_eta_line(eta_str) {
var eta_obj;
var eta_array;
var regex_eta = /^ETA:\s+(.*?)\s+([0-9]+)\s+s\s+([0-9]+)\/([0-9]+)\s+msgs\s+left\n?$/;
eta_array = regex_eta.exec(eta_str);
if (null !== eta_array) {
eta_obj = {
str: eta_str,
date: eta_array[1],
seconds_left: eta_array[2],
msgs_left: eta_array[3],
msgs_total: eta_array[4],
msgs_done: function () {
var diff = eta_obj.msgs_total - eta_obj.msgs_left;
return (diff.toString());
},
percent_done: function () {
var percent;
if (0 === eta_obj.msgs_total) {
return "0";
}
else {
percent = (eta_obj.msgs_total - eta_obj.msgs_left) / eta_obj.msgs_total * 100;
return (percent.toFixed(2));
}
},
percent_left: function () {
var percent;
if (0 === eta_obj.msgs_total) {
return "0";
}
else {
percent = (eta_obj.msgs_left / eta_obj.msgs_total * 100);
return (percent.toFixed(2));
}
}
};
}
else {
eta_obj = {
str: "",
date: "?",
seconds_left: "?",
msgs_left: "?",
msgs_total: "?",
msgs_done: "?",
percent_done: function () { return ""; },
percent_left: function () { return ""; }
};
}
return eta_obj;
};
var extract_eta = function extract_eta(xhr) {
var eta_obj;
var slice_length;
var slice_log;
var eta_str;
if (!xhr || typeof xhr.responseText !== "string") {
return {
str: "",
date: "?",
seconds_left: "?",
msgs_left: "?",
msgs_total: "?",
msgs_done: "?",
percent_done: function () { return ""; },
percent_left: function () { return ""; }
};
}
// wie bisher: nur den letzten Teil des Logs betrachten
if (xhr.readyState === 4) {
slice_length = -24000;
} else {
slice_length = -2400;
}
slice_log = xhr.responseText.slice(slice_length);
// 1. Versuch: originale ETA-Zeile (ETA: ... msgs left)
var eta_re = /ETA:.*\n/g;
var all_etas = slice_log.match(eta_re);
if (all_etas && all_etas.length) {
eta_str = all_etas[all_etas.length - 1];
eta_obj = decompose_eta_line(eta_str);
if (eta_obj && eta_obj.str && eta_obj.str.length) {
return eta_obj;
}
}
// 2. Fallback: nur "NNN/TTT msgs left" ohne strengen ETA-Header
var msgs_re = /(\d+)\s*\/\s*(\d+)\s+msgs\s+left/g;
var m;
var last = null;
while ((m = msgs_re.exec(slice_log)) !== null) {
last = m;
}
if (last) {
var left = parseInt(last[1], 10);
var total = parseInt(last[2], 10);
if (!isFinite(left) || !isFinite(total) || total === 0) {
// zur Sicherheit: zurück auf "unknown"
} else {
eta_obj = {
// künstliche ETA-String-Repräsentation
str: "ETA: ? ? s " + left + "/" + total + " msgs left\n",
date: "?",
seconds_left: "?",
msgs_left: left,
msgs_total: total,
msgs_done: function () {
return (eta_obj.msgs_total - eta_obj.msgs_left);
},
percent_done: function () {
var percent = (eta_obj.msgs_total - eta_obj.msgs_left) /
eta_obj.msgs_total * 100;
return percent.toFixed(2);
},
percent_left: function () {
var percent = eta_obj.msgs_left / eta_obj.msgs_total * 100;
return percent.toFixed(2);
}
};
return eta_obj;
}
}
// 3. Kein Fortschritts-Muster gefunden → "unknown"
return {
str: "",
date: "?",
seconds_left: "?",
msgs_left: "?",
msgs_total: "?",
msgs_done: "?",
percent_done: function () { return ""; },
percent_left: function () { return ""; }
};
};
var progress_bar_update = function progress_bar_update(eta_obj) {
if (eta_obj.str.length) {
$("#progress-bar-done").css("width", eta_obj.percent_done() + "%").attr("aria-valuenow", eta_obj.percent_done());
$("#progress-bar-left").css("width", eta_obj.percent_left() + "%").attr("aria-valuenow", eta_obj.percent_left());
$("#progress-bar-done").text(eta_obj.percent_done() + "% " + "done");
$("#progress-bar-left").text(eta_obj.percent_left() + "% " + "left");
}
else {
$("#progress-bar-done").text("unknown % " + "done");
$("#progress-bar-left").text("unknown % " + "left");
}
return;
};
var refreshLog = function refreshLog(xhr) {
var eta_obj;
var eta_str;
if (!xhr) {
return;
}
// ETA aus dem aktuellen Response-Text holen
eta_obj = extract_eta(xhr);
progress_bar_update(eta_obj);
if (xhr.readyState === 4) {
// Ende des Syncs
$("#progress-txt").text(
"Ended. It remains "
+ eta_obj.msgs_left + " messages to be synced"
);
} else {
eta_str = eta_obj.str + " (refresh done every " + refresh_interval_s + " s)";
eta_str = eta_str.replace(/(\r\n|\n|\r)/gm, ""); // Zeilenumbrüche trimmen
$("#progress-txt").text(eta_str);
}
};
var handleRun = function handleRun(xhr) {
if (!xhr) {
return;
}
var time = new Date();
// XHR-Status in #console schreiben
$("#console").append(
"Status: " + xhr.status + " " + xhr.statusText + "\n"
+ "State: " + readyStateStr[xhr.readyState] + "\n"
+ "Time: " + time + "\n\n"
);
// Progress/ETA aktualisieren
refreshLog(xhr);
if (xhr.readyState === 4) {
// Request fertig → Buttons wieder sinnvoll setzen
$("#bt-sync").prop("disabled", false);
$("#bt-abort").prop("disabled", true);
}
};
var imapsync = function imapsync() {
var querystring = $("#form").serialize();
$("#abort").text("\n\n\n"); // clean abort console
var consoleEl = document.getElementById("console");
var outputEl = document.getElementById("output");
// Startzustand
outputEl.textContent = "Here comes the log!\n\n";
// Zusätzliche Flags aus dem Original
if ("imap.gmail.com" === $("#host1").val()) {
querystring = querystring + "&gmail1=on";
}
if ("imap.gmail.com" === $("#host2").val()) {
querystring = querystring + "&gmail2=on";
}
if ("outlook.office365.com" === $("#host1").val()) {
querystring = querystring + "&office1=on";
}
if ("outlook.office365.com" === $("#host2").val()) {
querystring = querystring + "&office2=on";
}
if ("export.imap.mail.yahoo.com" === $("#host1").val()) {
querystring = querystring + "&yahoo1=on";
}
// URL aus dem Formular (HTML: action="/cgi-bin/imapsync")
var url = $("#form").attr("action") || "/cgi-bin/imapsync";
var xhr = new XMLHttpRequest();
var lastLength = 0; // wie weit responseText schon verarbeitet wurde
// Hilfsfunktion: nur neue Chunks anhängen
var appendChunk = function () {
var full = xhr.responseText || "";
var chunk = full.substring(lastLength);
if (chunk.length > 0) {
lastLength = full.length;
outputEl.textContent += chunk;
// immer ans Ende scrollen
outputEl.scrollTop = outputEl.scrollHeight;
}
};
xhr.open("POST", url, true);
xhr.setRequestHeader(
"Content-type",
"application/x-www-form-urlencoded"
);
// readyState-Änderung → Status + Progress
xhr.onreadystatechange = function () {
handleRun(xhr);
};
// bei jeder neuen Datenportion
xhr.onprogress = function () {
appendChunk(); // neue Log-Zeilen nach #output
handleRun(xhr); // Fortschrittsbalken + #console updaten
};
// am Ende sicherstellen, dass auch der letzte Rest dran ist
xhr.onload = function () {
appendChunk();
handleRun(xhr);
};
xhr.onerror = function () {
var msg = "\n--- XHR Fehler ---\n" +
"readyState=" + xhr.readyState + "\n" +
"status=" + xhr.status + " " + xhr.statusText + "\n";
outputEl.textContent += msg;
// Buttons wieder freigeben
$("#bt-sync").prop("disabled", false);
$("#bt-abort").prop("disabled", true);
};
xhr.send(querystring);
};
var handleAbort = function handleAbort(xhr) {
const time = new Date();
$("#abort").text(
"Status: " + xhr.status + " " + xhr.statusText + "\n"
+ "State: " + readyStateStr[xhr.readyState] + "\n"
+ "Time: " + time + "\n"
);
if (xhr.readyState === 4) {
$("#abort").append(xhr.responseText);
$("#bt-sync").prop("disabled", false);
$("#bt-abort").prop("disabled", false); // back for next abort
}
}
var abort = function abort() {
var querystring = $("#form").serialize() + "&abort=on";
var xhr;
xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
handleAbort(xhr);
};
xhr.open("POST", "/cgi-bin/imapsync", true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send(querystring);
}
var store = function store(id) {
var stored;
//$( "#tests" ).append( "Eco: " + id + " type is " + $( id ).attr( "type" ) + "\n" ) ;
if ("text" === $(id).attr("type") || "password" === $(id).attr("type")) {
localStorage.setItem(id, $(id).val());
stored = $(id).val();
}
else if ("checkbox" === $(id).attr("type")) {
//$( "#tests" ).append( "Eco: " + id + " checked is " + $( id )[0].checked + "\n" ) ;
localStorage.setItem(id, $(id)[0].checked);
stored = $(id)[0].checked;
}
return stored;
}
var retrieve = function retrieve(id) {
var retrieved;
//$( "#tests" ).append( "Eco: " + id + " type is " + $( id ).attr( "type" ) + " length is " + $( id ).length + "\n" ) ;
if ("text" === $(id).attr("type") || "password" === $(id).attr("type")) {
$(id).val(localStorage.getItem(id));
retrieved = $(id).val();
}
else if ("checkbox" === $(id).attr("type")) {
//$( "#tests" ).append( "Eco: " + id + " getItem is " + localStorage.getItem( id ) + "\n" ) ;
$(id)[0].checked = JSON.parse(localStorage.getItem(id));
retrieved = $(id)[0].checked;
}
return retrieved;
}
var tests_store_retrieve = function tests_store_retrieve() {
if ($("#tests").length !== 0) {
is(1, 1, "one equals one");
// isnot( 0, 1, "zero differs one" ) ;
// no exist
is(undefined, store("#test_noexists"),
"store: #test_noexists");
is(undefined, retrieve("#test_noexists"),
"retrieve: #test_noexists");
is(undefined, retrieve("#test_noexists2"),
"retrieve: #test_noexists2");
// input text
$("#test_text").val("foo");
is("foo", $("#test_text").val(), "#test_text val = foo");
is("foo", store("#test_text"), "store: #test_text");
$("#test_text").val("bar");
is("bar", $("#test_text").val(), "#test_text val = bar");
is("foo", retrieve("#test_text"), "retrieve: #test_text = foo");
is("foo", $("#test_text").val(), "#test_text val = foo");
// input check button
$("#test_checkbox").prop("checked", true);
is(true, store("#test_checkbox"), "store: #test_checkbox checked");
$("#test_checkbox").prop("checked", false);
is(true, retrieve("#test_checkbox"), "retrieve: #test_checkbox = true");
$("#test_checkbox").prop("checked", false);
is(false, store("#test_checkbox"), "store: #test_checkbox not checked");
$("#test_checkbox").prop("checked", true);
is(false, retrieve("#test_checkbox"), "retrieve: #test_checkbox = false");
}
}
var store_form = function store_form() {
if (Storage !== "undefined") {
// Code for localStorage.
store("#user1");
store("#password1");
store("#host1");
store("#subfolder1");
store("#showpassword1");
store("#user2");
store("#password2");
store("#host2");
store("#subfolder2");
store("#showpassword2");
store("#dry");
store("#justlogin");
store("#justfolders");
store("#justfoldersizes");
localStorage.account1_background_color = $("#account1").css("background-color");
localStorage.account2_background_color = $("#account2").css("background-color");
}
}
var show_extra_if_needed = function show_extra_if_needed() {
if ($("#subfolder1").length && $("#subfolder1").val().length > 0) {
$(".extra_param").show();
}
if ($("#subfolder2").length && $("#subfolder2").val().length > 0) {
$(".extra_param").show();
}
}
var retrieve_form = function retrieve_form() {
if (Storage !== "undefined") {
// Code for localStorage.
retrieve("#user1");
retrieve("#password1");
// retrieve("#showpassword1") ;
retrieve("#host1");
retrieve("#subfolder1");
retrieve("#user2");
retrieve("#password2");
// retrieve("#showpassword2") ;
retrieve("#host2");
retrieve("#subfolder2");
retrieve("#dry");
retrieve("#justlogin");
retrieve("#justfolders");
retrieve("#justfoldersizes");
// In case, how to restore the original color from css file.
// localStorage.removeItem( "account1_background_color" ) ;
// localStorage.removeItem( "account2_background_color" ) ;
if (localStorage.account1_background_color) {
$("#account1").css("background-color",
localStorage.account1_background_color);
}
if (localStorage.account2_background_color) {
$("#account2").css("background-color",
localStorage.account2_background_color);
}
// Show the extra parameters if they are not empty because it would
// be dangerous to retrieve them without showing them
show_extra_if_needed();
}
}
var showpassword = function showpassword(id, button) {
var x = document.getElementById(id);
if (button.checked) {
x.type = "text";
} else {
x.type = "password";
}
}
var tests_cryptojs = function tests_cryptojs() {
if ($("#tests").length !== 0) {
if (typeof CryptoJS === 'undefined') {
is(true, typeof CryptoJS !== 'undefined', "CryptoJS is available");
note("CryptoJS is not available on this site. Ask the admin to fix this.\n");
}
else if (typeof CryptoJS.SHA256 !== "function") {
is("function", typeof CryptoJS.SHA256, "CryptoJS.SHA256 is a function");
note("CryptoJS.SHA256 function is not available on this site. Ask the admin to fix this.\n");
}
else {
// safe to use the function
is("function", typeof CryptoJS.SHA256, "CryptoJS.SHA256 is a function");
is("2f77668a9dfbf8d5848b9eeb4a7145ca94c6ed9236e4a773f6dcafa5132b2f91", sha256("Message"), "sha256 Message");
is("26429a356b1d25b7d57c0f9a6d5fed8a290cb42374185887dcd2874548df0779", sha256("caca"), "sha256 caca");
is("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", sha256(""), "sha256 ''");
is(tmphash(), tmphash(), "tmphash");
$("#user1").val("test1");
$("#password1").val("secret1");
$("#host1").val("test1.lamiral.info");
$("#user2").val("test2");
$("#password2").val("secret2");
$("#host2").val("test2.lamiral.info");
is("20d2b4917cf69114876b4c8779af543e89c5871c6ada68107619722e55af1101", tmphash(), "tmphash like testslive");
$("#user1").val("");
$("#password1").val("");
$("#host1").val("");
$("#user2").val("");
$("#password2").val("");
$("#host2").val("");
}
}
}
var sha256 = function sha256(string) {
var hash = CryptoJS.SHA256(string);
var hash_hex = hash.toString(CryptoJS.enc.Hex);
return (hash_hex);
}
var tmphash = function tmphash() {
var string = "";
string = string.concat(
$("#user1").val(), $("#password1").val(), $("#host1").val(),
$("#user2").val(), $("#password2").val(), $("#host2").val(),
)
return (sha256(string));
}
var init = function init() {
// in case of a manual refresh, start with
$("#bt-sync").prop("disabled", false);
$("#bt-abort").prop("disabled", false);
$("#progress-bar-left").css("width", 100 + "%").attr("aria-valuenow", 100);
$("#showpassword1").on('click', function (event) {
var button = event.target;
showpassword("password1", button);
}
);
$("#showpassword2").on('click', function (event) {
//$("#tests").append( "\nthat1=" + JSON.stringify( event.target, undefined, 4 ) ) ;
var button = event.target;
showpassword("password2", button);
}
);
$("#bt-sync").on('click', function () {
$("#bt-sync").prop("disabled", true);
$("#bt-abort").prop("disabled", false);
$("#progress-txt").text("ETA: coming soon");
store_form();
imapsync();
}
);
$("#bt-abort").on('click', function () {
$("#bt-sync").prop("disabled", true);
$("#bt-abort").prop("disabled", true);
abort();
}
);
var swap = function swap(p1, p2) {
var temp = $(p2).val();
$(p2).val($(p1).val());
$(p1).val(temp);
};
$("#swap").on('click', function () {
// swaping colors can't use swap()
var temp1 = $("#account1").css("background-color");
var temp2 = $("#account2").css("background-color");
$("#account1").css("background-color", temp2);
$("#account2").css("background-color", temp1);
swap($("#user1"), $("#user2"));
swap($("#password1"), $("#password2"));
swap($("#host1"), $("#host2"));
swap($("#subfolder1"), $("#subfolder2"));
var temp = $("#showpassword1")[0].checked;
$("#showpassword1")[0].checked = $("#showpassword2")[0].checked;
$("#showpassword2")[0].checked = temp;
showpassword("password1", $("#showpassword1")[0]);
showpassword("password2", $("#showpassword2")[0]);
}
);
}
var tests_bilan = function tests_bilan(nb_attended_test) {
// attended number of tests: nb_attended_test
$("#tests").append("1.." + test.counter_all + "\n");
if (test.counter_nok > 0) {
$("#tests").append(
"\nFAILED tests \n"
+ test.failed_tests
);
$("#tests").collapse("show");
}
// Summary of tests: failed 0 tests, run xx tests,
// expected to run yy tests.
if (test.counter_all !== nb_attended_test) {
$("#tests").append("# Looks like you planned "
+ nb_attended_test
+ " tests but ran "
+ test.counter_all + ".\n"
);
$("#tests").collapse("show");
}
};
var tests = function tests(nb_attended_test) {
if ($("#tests").length !== 0) {
tests_store_retrieve();
tests_last_eta();
tests_decompose_eta_line();
tests_last_x_lines();
// tests_cryptojs( ) ;
// The following test can be used to check that if a test fails
// then all the tests are shown to the user.
//is( 0, 1, "this test always fails" ) ;
tests_bilan(nb_attended_test);
// If you want to always see the tests, uncomment the following
// line
//$("#tests").collapse("show") ;
}
}
$(document).ready(
function () {
"use strict";
// Bootstrap popover and tooltip
$("[data-toggle='tooltip']").tooltip();
init();
//tests(38);
retrieve_form();
}
);