diff --git a/lib/Ajax/Application.php b/lib/Ajax/Application.php index 5fe6f7e93..e9241aa5f 100644 --- a/lib/Ajax/Application.php +++ b/lib/Ajax/Application.php @@ -283,11 +283,6 @@ public function changed($rw = null) return true; } - /* Only update search mailboxes on forced refreshes. */ - if ($this->indices->mailbox->search) { - return !empty($this->_vars->forceUpdate); - } - if (!$this->_vars->viewport->cacheid) { return false; } @@ -414,6 +409,11 @@ public function getAddrFields() */ public function deleteMsgs(IMP_Indices $indices, $changed, $force = false) { + /* Search results may have been invalidated during deletion. */ + if (!$changed) { + $changed = $this->changed(true); + } + /* Check if we need to update thread information. */ if (!$changed) { $changed = ($this->indices->mailbox->getSort()->sortby == Horde_Imap_Client::SORT_THREAD); diff --git a/lib/Contents.php b/lib/Contents.php index f84705a02..7546acd22 100644 --- a/lib/Contents.php +++ b/lib/Contents.php @@ -1322,8 +1322,14 @@ public function getPartName(Horde_Mime_Part $part, $use_descrip = false) */ protected function _fetchData(Horde_Imap_Client_Fetch_Query $query) { + $mbox = $this->getMailbox(); + if (!$mbox) { + $e = new IMP_Exception(_('Error displaying message: message does not exist on server.')); + $e->setLogLevel('NOTICE'); + throw $e; + } + try { - $mbox = $this->getMailbox(); $imp_imap = $mbox->imp_imap; return $imp_imap->fetch($mbox, $query, [ 'ids' => $imp_imap->getIdsOb($this->getUid()), diff --git a/lib/Indices.php b/lib/Indices.php index 84f64d84d..b42ed8545 100644 --- a/lib/Indices.php +++ b/lib/Indices.php @@ -291,6 +291,7 @@ public function copy($targetMbox, $action, array $opts = []) } $imap_move = false; $return_value = true; + $affected_mailboxes = []; switch ($action) { case 'move': @@ -326,6 +327,11 @@ public function copy($targetMbox, $action, array $opts = []) 'ids' => $imp_imap->getIdsOb($ob->uids), 'move' => $imap_move, ]); + + if ($imap_move) { + $affected_mailboxes[strval($ob->mbox)] = $ob->mbox; + $affected_mailboxes[strval($targetMbox)] = $targetMbox; + } } catch (Exception $e) { $error_msg = sprintf( $message, @@ -343,6 +349,12 @@ public function copy($targetMbox, $action, array $opts = []) } } + if ($return_value && $imap_move && !empty($affected_mailboxes)) { + $GLOBALS['injector']->getInstance('IMP_Search')->invalidateMailboxes( + array_values($affected_mailboxes) + ); + } + return $return_value; } @@ -385,6 +397,7 @@ public function delete(array $opts = []) ? $injector->getInstance('IMP_Maillog') : null; $return_value = 0; + $affected_mailboxes = []; /* Check for Trash mailbox. */ $no_expunge = $use_trash_mbox = $use_vtrash = false; @@ -428,12 +441,9 @@ public function delete(array $opts = []) continue; } - if ($return_value !== false) { - $return_value += count($ob->uids); - } - $imp_imap = $ob->mbox->imp_imap; $ids_ob = $imp_imap->getIdsOb($ob->uids); + $processed = false; /* Trash is only valid for IMAP mailboxes. */ if ($use_trash_mbox @@ -453,6 +463,9 @@ public function delete(array $opts = []) 'ids' => $ids_ob, 'move' => true, ]); + $affected_mailboxes[strval($ob->mbox)] = $ob->mbox; + $affected_mailboxes[strval($trash)] = $trash; + $processed = true; } catch (IMP_Imap_Exception $e) { if ($e->getCode() == $e::OVERQUOTA) { $notification->push( @@ -510,9 +523,22 @@ public function delete(array $opts = []) new IMP_Indices($ob->mbox, $ids_ob) ); } + + $affected_mailboxes[strval($ob->mbox)] = $ob->mbox; + $processed = true; } catch (IMP_Imap_Exception $e) { } } + + if ($processed && ($return_value !== false)) { + $return_value += count($ob->uids); + } + } + + if ($return_value !== false && !empty($affected_mailboxes)) { + $injector->getInstance('IMP_Search')->invalidateMailboxes( + array_values($affected_mailboxes) + ); } return $return_value; diff --git a/lib/Mailbox.php b/lib/Mailbox.php index 8c4677293..1983c070c 100644 --- a/lib/Mailbox.php +++ b/lib/Mailbox.php @@ -1363,6 +1363,15 @@ public function expunge($to_expunge = [], array $opts = []) } } + if (!empty($update_list)) { + $GLOBALS['injector']->getInstance('IMP_Search')->invalidateMailboxes( + array_filter(array_map( + [IMP_Mailbox::class, 'get'], + array_keys($update_list) + )) + ); + } + if ($msg_list) { return new IMP_Indices($update_list); } @@ -1591,8 +1600,8 @@ public static function prefTo($mbox) * from the mailbox. Additionally, if CONDSTORE is available on the remote * IMAP server, this ID will change if flag information changes. * - * For search mailboxes, this value never changes (search mailboxes must - * be forcibly refreshed). + * For search mailboxes, this value changes when underlying mailbox + * contents change or when search display preferences change. * * @param boolean $date If true, adds date information to ID. * @@ -1601,16 +1610,12 @@ public static function prefTo($mbox) */ protected function _getCacheID($date = false) { - global $prefs; + global $injector, $prefs; $date = $date ? 'D' . date('z') : ''; - if ($this->search) { - return '1' . ($date ? '|' . $date : ''); - } - $sortpref = $this->getSort(true); $addl = [ $sortpref->sortby, @@ -1621,6 +1626,16 @@ protected function _getCacheID($date = false) $addl[] = $date; } + if ($this->search) { + $cache_gen = 0; + $imp_search = $injector->getInstance('IMP_Search'); + if ($imp_search->offsetExists($this->_mbox)) { + $cache_gen = $imp_search[$this->_mbox]->cacheGeneration(); + } + + return $cache_gen . '|' . implode('|', $addl); + } + try { return $this->imp_imap->getCacheId($this->_mbox, $addl); } catch (IMP_Imap_Exception $e) { diff --git a/lib/Search.php b/lib/Search.php index ce28537d4..e78aac429 100644 --- a/lib/Search.php +++ b/lib/Search.php @@ -445,6 +445,65 @@ public function isSearchMbox($id) return (strpos($id, self::MBOX_PREFIX) === 0); } + /** + * Invalidate cached search results for queries that include the given + * mailboxes. + * + * @param array $mboxes List of IMP_Mailbox objects and/or mailbox names. + */ + public function invalidateMailboxes(array $mboxes) + { + global $injector; + + if (empty($mboxes)) { + return; + } + + $mboxes = array_unique(array_filter(array_map( + 'strval', + array_map([IMP_Mailbox::class, 'get'], $mboxes) + ))); + + $list_factory = $injector->getInstance('IMP_Factory_MailboxList'); + + foreach (['query', 'vfolders'] as $type) { + foreach ($this->_search[$type] as $query) { + if (!$this->_queryAffected($query, $mboxes)) { + continue; + } + + $query->invalidateCache(); + $list_factory->create($query->mid)->rebuild(true); + $this->changed = true; + } + } + } + + /** + * Does a search query include any of the given mailboxes? + * + * @param IMP_Search_Query $query Search query object. + * @param array $mboxes List of mailbox names. + * + * @return boolean + */ + protected function _queryAffected(IMP_Search_Query $query, array $mboxes) + { + if ($query->all) { + return true; + } + + $mboxes = array_flip($mboxes); + + foreach ($query->mboxes as $mbox) { + if (isset($mboxes[strval($mbox)])) { + return true; + } + } + + return false; + } + /** * Strip the identifying label from a mailbox ID. * diff --git a/lib/Search/Query.php b/lib/Search/Query.php index 6e33f2ba0..7863fe0ce 100644 --- a/lib/Search/Query.php +++ b/lib/Search/Query.php @@ -65,6 +65,13 @@ class IMP_Search_Query implements Serializable */ protected $_cache = []; + /** + * Cache generation. Incremented when underlying mailbox contents change. + * + * @var integer + */ + protected $_cacheGen = 0; + /** * Can this query be edited? * @@ -326,6 +333,25 @@ public function replace(array $criteria = []) } } + /** + * Return the cache generation for this query. + * + * @return integer + */ + public function cacheGeneration() + { + return $this->_cacheGen; + } + + /** + * Invalidate cached search results for this query. + */ + public function invalidateCache() + { + ++$this->_cacheGen; + $this->_cache = []; + } + /** * Reduce the sorted return ID list by running search element callbacks.. * @@ -366,6 +392,7 @@ public function serialize() 'm' => $this->_mboxes, 'v' => self::VERSION, ]); + $data['g'] = $this->_cacheGen; foreach ($this->_nosave as $val) { unset($data[$val]); @@ -383,6 +410,7 @@ public function __serialize(): array 'm' => $this->_mboxes, 'v' => self::VERSION, ]); + $data['g'] = $this->_cacheGen; foreach ($this->_nosave as $val) { unset($data[$val]); @@ -420,6 +448,9 @@ public function unserialize($data) if (isset($data['m'])) { $this->_mboxes = $data['m']; } + if (isset($data['g'])) { + $this->_cacheGen = $data['g']; + } } public function __unserialize(array $data): void { @@ -441,5 +472,8 @@ public function __unserialize(array $data): void if (isset($data['m'])) { $this->_mboxes = $data['m']; } + if (isset($data['g'])) { + $this->_cacheGen = $data['g']; + } } }