// Copyright 2023 Arnaud Ferraris, Oliver Smith
// Copyright 2024-2025 Dylan Van Assche
// SPDX-License-Identifier: MPL-2.0
//
// Generate and update userChrome.css and userContent.css for the user's
// profile from CSS fragments in /etc/mobile-config-thunderbird, depending on the
// installed Thunderbird version. Set various defaults for about:config options in
// set_default_prefs().
//
// Log file:
// $ find ~/.mozilla -name mobile-config-thunderbird.log
//
// This is a Thunderbord autoconfig file:
// https://enterprise.thunderbird.net/deploy/mcd-thunderbird-autoconfig
//
// The XPCOM APIs used here are the same as old Thunderbird add-ons used, and the
// documentation for them has been removed (can we use something else? patches
// welcome). They appear to still work fine for autoconfig scripts.
// https://web.archive.org/web/20201018211550/https://developer.mozilla.org/en-US/docs/Archive/Add-ons/Code_snippets/File_I_O

const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const Services = globalThis.Services;
const { AppConstants } = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");
const IS_ESM_READY = parseInt(AppConstants.MOZ_APP_VERSION, 10) >= 128;

// We need to conditionally load some modules because they haven't been ported
// the ES module yet. This workaround can be removed when ESR128 will be EOL.
const { FileUtils } =
    IS_ESM_READY
      ? ChromeUtils.importESModule("resource://gre/modules/FileUtils.sys.mjs")
      : Cu.import("resource://gre/modules/FileUtils.jsm");

var g_tb_version;
var g_updated = false;
var g_fragments_cache = {}; // cache for css_file_get_fragments()
var g_logFileStream;
var g_chromeDir; // nsIFile object for the "chrome" dir in user's profile


function write_line(ostream, line) {
    line = line + "\n"
    ostream.write(line, line.length);
}

// Create <profile>/chrome/ directory if not already present
function chrome_dir_init() {
    g_chromeDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
    g_chromeDir.append("chrome");
    if (!g_chromeDir.exists()) {
        g_chromeDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
    }
}

function log_init() {
    var mode = FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_APPEND;
    var logFile = g_chromeDir.clone();
    logFile.append("mobile-config-thunderbird.log");
    g_logFileStream = FileUtils.openFileOutputStream(logFile, mode);
}

function log(line) {
    var date = new Date().toISOString().replace("T", " ").slice(0, 19);
    line = "[" + date + "] " + line;
    write_line(g_logFileStream, line);
}

// Debug function for logging object attributes
function log_obj(obj) {
    var prop;
    var value;

    for (var prop in obj) {
        try {
            value = obj[prop];
        } catch(e) {
            value = e;
        }
        log(" - " + prop + ": " + value);
    }
}

function get_thunderbird_version() {
    return Services.appinfo.version.split(".")[0];
}

function get_thunderbird_version_previous() {
    var file = g_chromeDir.clone();
    file.append("tb_previous.txt");

    if (!file.exists())
        return "unknown";

    var istream = Cc["@mozilla.org/network/file-input-stream;1"].
                  createInstance(Components.interfaces.nsIFileInputStream);
    istream.init(file, 0x01, 0444, 0);
    istream.QueryInterface(Components.interfaces.nsILineInputStream);

    var line = {};
    istream.readLine(line);
    istream.close();

    return line.value.trim();
}

function set_thunderbird_version_previous(new_version) {
    log("Updating previous Thunderbird version to: " + new_version);

    var file = g_chromeDir.clone();
    file.append("tb_previous.txt");

    var ostream = Cc["@mozilla.org/network/file-output-stream;1"].
                  createInstance(Components.interfaces.nsIFileOutputStream);
    ostream.init(file, 0x02 | 0x08 | 0x20, 0644, 0);
    write_line(ostream, new_version);
    ostream.close();
}

function trigger_thunderbird_restart() {
    log("Triggering Thunderbird restart");
    var appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
    appStartup.quit(Ci.nsIAppStartup.eForceQuit | Ci.nsIAppStartup.eRestart);
}

// Check if a CSS fragment should be used or not, depending on the current
// Thunderbird version.
// fragment: e.g. "userChrome/popups.before-tb-108.css"
// returns: true if it should be used, false if it must not be used
function css_fragment_check_thunderbird_version(fragment) {
    if (fragment.indexOf(".before-tb-") !== -1) {
        var before_tb_version = fragment.split("-").pop().split(".")[0];
        if (g_tb_version >= before_tb_version) {
            log("Fragment with Thunderbird version check not included: " + fragment);
            return false;
        } else {
            log("Fragment with Thunderbird version check included: " + fragment);
            return true;
        }
    }

    return true;
}

// Get an array of paths to the fragments for one CSS file
// name: either "userChrome" or "userContent"
function css_file_get_fragments(name) {
    if (name in g_fragments_cache)
        return g_fragments_cache[name];

    var ret = [];
    var path = "/etc/mobile-config-thunderbird/" + name + ".files";
    log("Reading fragments from file: " + path);
    var file = new FileUtils.File(path);

    var istream = Cc["@mozilla.org/network/file-input-stream;1"].
                  createInstance(Components.interfaces.nsIFileInputStream);
    istream.init(file, 0x01, 0444, 0);
    istream.QueryInterface(Components.interfaces.nsILineInputStream);

    var has_more;
    do {
        var line = {};
        has_more = istream.readLine(line);
        if (css_fragment_check_thunderbird_version(line.value))
            ret.push("/etc/mobile-config-thunderbird/" + line.value);

    } while (has_more);

    istream.close();

    g_fragments_cache[name] = ret;
    return ret;
}

// Create a nsIFile object with one of the CSS files in the user's profile as
// path. The file doesn't need to exist at this point.
// name: either "userChrome" or "userContent"
function css_file_get(name) {
    var ret = g_chromeDir.clone();
    ret.append(name + ".css");
    return ret;
}

// Delete either userChrome.css or userContent.css inside the user's profile if
// they have an older timestamp than the CSS fragments (or list of CSS
// fragments) installed system-wide.
// name: either "userChrome" or "userContent"
// file: return of css_file_get()
function css_file_delete_outdated(name, file) {
    var depends = css_file_get_fragments(name).slice(); /* copy the array */
    depends.push("/etc/mobile-config-thunderbird/" + name + ".files");
    for (var i in depends) {
        var depend = depends[i];
        var file_depend = new FileUtils.File(depend);

        if (file.lastModifiedTime < file_depend.lastModifiedTime) {
            log("Removing outdated file: " + file.path + " (newer: "
                + depend + ")");
            file.remove(false);
            return;
        }
    }

    log("File is up-to-date: " + file.path);
    return;
}

// Create userChrome.css / userContent.css in the user's profile, based on the
// CSS fragments stored in /etc/mobile-config-thunderbird.
// name: either "userChrome" or "userContent"
// file: return of css_file_get()
function css_file_merge(name, file) {
    log("Creating CSS file from fragments: " + file.path);

    var ostream = Cc["@mozilla.org/network/file-output-stream;1"].
                  createInstance(Components.interfaces.nsIFileOutputStream);
    ostream.init(file, 0x02 | 0x08 | 0x20, 0644, 0);

    var fragments = css_file_get_fragments(name);
    for (var i in fragments) {
        var line;
        var fragment = fragments[i];
        log("- " + fragment);
        write_line(ostream, "/* === " + fragment + " === */");

        var file_fragment = new FileUtils.File(fragment);

        var istream = Cc["@mozilla.org/network/file-input-stream;1"].
                      createInstance(Components.interfaces.nsIFileInputStream);
        istream.init(file_fragment, 0x01, 0444, 0);
        istream.QueryInterface(Components.interfaces.nsILineInputStream);

        var has_more;
        do {
            var line = {};
            has_more = istream.readLine(line);
            write_line(ostream, line.value);
        } while (has_more);

        istream.close();
    }

    ostream.close();
    g_updated = true;
}

function css_files_update() {
    g_tb_version = get_thunderbird_version();
    var tb_previous = get_thunderbird_version_previous();
    log("Thunderbird version: " + g_tb_version + " (previous: " + tb_previous + ")");

    var names = ["userChrome", "userContent"];
    for (var i in names) {
        var name = names[i];
        var file = css_file_get(name);

        if (file.exists()) {
            if (g_tb_version != tb_previous) {
                log("Removing outdated file: " + file.path + " (Thunderbird" +
                    " version changed)");
                file.remove(false);
            } else {
                css_file_delete_outdated(name, file);
            }
        }

        if (!file.exists()) {
            css_file_merge(name, file);
        }
    }

    if (g_tb_version != tb_previous)
        set_thunderbird_version_previous(g_tb_version);
}

/**
 * Builds a user-agent as similar to the default as possible, but with "Mobile"
 * inserted into the platforms section.
 *
 * @returns {string}
 */
function build_user_agent() {
    var appinfo = Services.appinfo;
    var vendor = appinfo.vendor || "Mozilla";
    var os = appinfo.OS || "Linux";
    var version = get_thunderbird_version() + ".0";
    var name = appinfo.name || "Thunderbird";
    var arch = (appinfo.XPCOMABI && appinfo.XPCOMABI.includes("-"))
        ? appinfo.XPCOMABI.split("-")[0]
        : "aarch64";

    return `${vendor}/5.0 (X11; ${os} ${arch}; Mobile; rv:${version}) Gecko/20100101 ${name}/${version}`;
}

function set_default_prefs() {
    log("Setting default preferences");

    // Always present 'Mobile' in the UA for HTML e-mails with a mobile responsiveness
    var user_agent = build_user_agent();
    defaultPref('general.useragent.override', user_agent);
    log("UA set to: " + user_agent)

    // Make navigator.maxTouchPoints return 1 for clients to determine this is a
    // touch device. This is the same value used by Web Developer Tools ->
    // Responsive Design Mode -> Enable touch simulation.
    defaultPref('dom.maxtouchpoints.testing.value', 1);

    // Use the xdg-desktop-portal.file-picker by default, e.g., for a native
    // file-picker instead of gtk-file-picker on Plasma Mobile
    defaultPref('widget.use-xdg-desktop-portal.file-picker', 1);

    // Hide spaces toolbar
    var docURL = "chrome://messenger/content/messenger.xhtml";
    Services.xulStore.setValue(docURL, "spacesToolbar", "hidden", true);

    // Configure message header to be more compact:
    // - Hide avatars
    // - Hide full e-mailaddresses
    // - Smaller subject line
    // - Hide labels
    // - Only icons for buttons
    var docURL = "chrome://messenger/content/messenger.xhtml";
    Services.xulStore.setValue(docURL, "messageHeader", "layout",
    "{\"showAvatar\":false,\"showBigAvatar\":false,\"showFullAddress\":false,\"hideLabels\":true,\"subjectLarge\":false,\"buttonStyle\":\"only-icons\"}");

    // Hide the message pane as we open e-mails in a new window
    var docURL = "chrome://messenger/content/messenger.xhtml";
    Services.xulStore.setValue(docURL, "messagepaneboxwrapper", "collapsed", true);

    // Add show/hide folder pane button
    var docURL = "chrome://messenger/content/messenger.xhtml";
    Services.xulStore.setValue(docURL, "folderPaneBox", "width", 360);

    // Move folder pane button to the left
    var docURL = "chrome://messenger/content/messenger.xhtml";
    Services.xulStore.setValue(docURL, "unifiedToolbar", "state",
    "{\"mail\":[\"ext-mobileConfigThunderbird@postmarketOS\",\"search-bar\"]}");

    // Show icons and text under each other when composing messages in the compose toolbar
    var docURL = "chrome://messenger/content/messengercompose/messengercompose.xhtml";
    Services.xulStore.setValue(docURL, "compose-toolbox", "mode", "full");

    // Show minimal number of icons in messenger compose toolbar
    var docURL = "chrome://messenger/content/messengercompose/messengercompose.xhtml";
    Services.xulStore.setValue(docURL, "composeToolbar2", "mode", "full");
    var docURL = "chrome://messenger/content/messengercompose/messengercompose.xhtml";
    Services.xulStore.setValue(docURL, "composeToolbar2", "currentset",
    "button-send,separator,button-encryption,separator,spellingButton,button-save,button-attach");
}

function main() {
    log("Running mobile-config-autoconfig.js");
    css_files_update();

    // Restart Thunderbird immediately if one of the files got updated
    if (g_updated == true)
        trigger_thunderbird_restart();
    else
        set_default_prefs();

    log("Done");
}

chrome_dir_init();
log_init();
try {
    main();
} catch(e) {
    log("main() failed: " + e);

    // Let Thunderbird display the generic error message that something went wrong
    // in the autoconfig script.
    error;
}
g_logFileStream.close();
