<?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"] ?? [];
}
}