/ src / models / MailModel.php
<?php
/**
 * 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
 */
namespace seekquarry\yioop\models;

use seekquarry\yioop\library\mail\ImapListing;

/**
 * Holds the small pieces of per-user state the Mail activity keeps
 * between one inbox page and the next: which way the open folder is
 * sorted, the ordered message list a sorted view produced (so paging
 * does not re-sort), and whether the account's mail server can sort
 * at all. All of it lives in the browsing session rather than the
 * database, because it is a transient view preference, not a stored
 * record. Keeping it here lets the Mail controller stay a thin
 * request handler instead of carrying session bookkeeping.
 *
 * @author Chris Pollett
 */
class MailModel extends Model
{
    /**
     * How long, in seconds, a cached folder list stays fresh before
     * the inbox fetches it from the mail server again. Five minutes
     * keeps the folder side-panel responsive without holding on to a
     * stale list across a long session.
     */
    const MAIL_FOLDERS_CACHE_TTL = 300;
    /**
     * Remembers how a folder is sorted for one account, so the choice
     * sticks as the user moves around the inbox.
     *
     * @param int $account_id the mail account being viewed
     * @param string $folder the folder being viewed
     * @param array $sort a ["key" => string, "reverse" => bool] pair
     */
    public function storeSort($account_id, $folder, $sort)
    {
        if (empty($_SESSION["MAIL_SORT"])) {
            $_SESSION["MAIL_SORT"] = [];
        }
        if (empty($_SESSION["MAIL_SORT"][$account_id])) {
            $_SESSION["MAIL_SORT"][$account_id] = [];
        }
        $_SESSION["MAIL_SORT"][$account_id][$folder] = $sort;
    }

    /**
     * Returns the remembered sort for a folder, or null when none has
     * been chosen yet. The caller validates it, since what counts as
     * a legal sort is a controller-level decision.
     *
     * @param int $account_id the mail account being viewed
     * @param string $folder the folder being viewed
     * @return array|null the remembered ["key", "reverse"] pair, or
     *      null if nothing is stored
     */
    public function storedSort($account_id, $folder)
    {
        return $_SESSION["MAIL_SORT"][$account_id][$folder] ?? null;
    }

    /**
     * Remembers the ordered list of message sequence numbers a sorted
     * view produced, so paging further into that view does not have
     * to ask the server to sort again. The list is kept per account,
     * folder, filter, and sort so different views of the same folder
     * do not overwrite each other. A view with no such list (the
     * plain newest-first one) stores nothing.
     *
     * @param int $account_id the account being viewed
     * @param string $folder the folder being viewed
     * @param string $filter the free-text filter in effect, or empty
     * @param array $sort the sort in effect
     * @param bool $unread_only the unread-only toggle
     * @param bool $flagged_only the flagged-only toggle
     * @param array|null $cursor the listing's next_cursor, which holds
     *      the ordered list when there is one to remember
     */
    public function cacheSequence($account_id, $folder, $filter, $sort,
        $unread_only, $flagged_only, $cursor)
    {
        if (!isset($cursor['sequence'])) {
            return;
        }
        $signature = ImapListing::sortSignature($sort, $unread_only,
            $flagged_only);
        if (empty($_SESSION["MAIL_SORT_LIST"])) {
            $_SESSION["MAIL_SORT_LIST"] = [];
        }
        $_SESSION["MAIL_SORT_LIST"][$account_id][$folder][$filter]
            [$signature] = $cursor['sequence'];
    }

    /**
     * Returns the remembered ordered list for a sorted view so a
     * "load more" can continue where it left off without re-sorting,
     * or an empty list when none was kept.
     *
     * @param int $account_id the account being viewed
     * @param string $folder the folder being viewed
     * @param string $filter the free-text filter in effect, or empty
     * @param string $signature the sort/toggle signature identifying
     *      the view
     * @return array the ordered sequence numbers, or an empty array
     */
    public function cachedSequence($account_id, $folder, $filter,
        $signature)
    {
        return $_SESSION["MAIL_SORT_LIST"][$account_id][$folder]
            [$filter][$signature] ?? [];
    }

    /**
     * Returns whether an account's mail server has already been found
     * to support sorting, so the inbox can skip asking the server
     * again on later pages. Returns null when it has not been learned
     * yet.
     *
     * @param int $account_id the account being viewed
     * @return bool|null the remembered support, or null if unknown
     */
    public function sortSupportHint($account_id)
    {
        return $_SESSION["MAIL_SORT_SUPPORTED"][$account_id] ?? null;
    }

    /**
     * Remembers whether an account's mail server can sort messages so
     * later inbox pages do not have to ask it again.
     *
     * @param int $account_id the account being viewed
     * @param bool $supported whether the server can sort
     */
    public function cacheSortSupport($account_id, $supported)
    {
        if (empty($_SESSION["MAIL_SORT_SUPPORTED"])) {
            $_SESSION["MAIL_SORT_SUPPORTED"] = [];
        }
        $_SESSION["MAIL_SORT_SUPPORTED"][$account_id] =
            (bool) $supported;
    }

    /**
     * Returns an account's cached folder list when it is still fresh,
     * or null when nothing is cached or the cache has aged out. The
     * inbox uses this to avoid re-asking the mail server for the
     * folder list on every page load. Freshness is tracked by a
     * companion expiry timestamp stamped when the list was cached.
     *
     * @param int $account_id the account whose folder list is wanted
     * @return array|null the folder list as parsed by
     *      ImapFolderListParser, or null when empty or stale
     */
    public function cachedFolders($account_id)
    {
        $folders = $_SESSION["MAIL_FOLDERS"][$account_id] ?? null;
        if (!is_array($folders) || empty($folders)) {
            return null;
        }
        $expires =
            $_SESSION["MAIL_FOLDERS_FRESH_UNTIL"][$account_id] ?? 0;
        if ($expires <= time()) {
            return null;
        }
        return $folders;
    }

    /**
     * Caches a freshly fetched folder list for an account and stamps
     * how long it should be trusted, so later pages can reuse it
     * rather than fetch it from the mail server again.
     *
     * @param int $account_id the account whose list is being cached
     * @param array $folders the folder list parsed and sorted by
     *      ImapFolderListParser
     */
    public function cacheFolders($account_id, $folders)
    {
        $_SESSION["MAIL_FOLDERS"][$account_id] = $folders;
        $_SESSION["MAIL_FOLDERS_FRESH_UNTIL"][$account_id] =
            time() + self::MAIL_FOLDERS_CACHE_TTL;
    }

    /**
     * Drops an account's cached folder list and its freshness stamp
     * so the next view rebuilds it from the server. Used after the
     * folder set changes (a folder created, renamed, or deleted) or
     * the account itself is removed.
     *
     * @param int $account_id the account whose cache to clear
     */
    public function invalidateFolders($account_id)
    {
        unset($_SESSION["MAIL_FOLDERS"][$account_id]);
        unset($_SESSION["MAIL_FOLDERS_FRESH_UNTIL"][$account_id]);
    }

    /**
     * Returns whatever folder list is cached for an account, or an
     * empty list when none is, without checking freshness. Used
     * where any reasonably recent list will do -- finding a target
     * folder for a cross-account move, or confirming a folder exists
     * before a folder operation -- rather than for the
     * freshness-sensitive inbox render.
     *
     * @param int $account_id the account whose cached list is wanted
     * @return array the cached folder list, or an empty array
     */
    public function foldersFor($account_id)
    {
        return $_SESSION["MAIL_FOLDERS"][$account_id] ?? [];
    }

    /**
     * Returns the cached folder lists for every account the session
     * has visited, keyed by account id. The Mail page hands this to
     * the view so every pane can show the folder side-panel, not
     * only the pane that happened to fetch its own list.
     *
     * @return array map of account id to that account's cached
     *      folder list
     */
    public function allCachedFolders()
    {
        return $_SESSION["MAIL_FOLDERS"] ?? [];
    }
}
X