feat(search-ui): optimize search UI

This commit is contained in:
rustmailer
2026-01-19 01:52:07 +08:00
parent 48f8092b5a
commit 0d20a9676a
33 changed files with 2129 additions and 450 deletions
+30 -3
View File
@@ -16,6 +16,8 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use std::collections::HashSet;
use crate::modules::account::migration::AccountModel;
use crate::modules::cache::imap::mailbox::MailBox;
use crate::modules::error::code::ErrorCode;
@@ -155,9 +157,9 @@ impl Envelope {
let id = create_hash(account_id, &message_id);
let full_text = extract_string_field(doc, fields.f_text)?;
// Take up to the first 120 characters as a preview;
let preview = if full_text.chars().count() > 120 {
full_text.chars().take(120).collect::<String>() + "..."
// Take up to the first 500 characters as a preview;
let preview = if full_text.chars().count() > 500 {
full_text.chars().take(500).collect::<String>() + "..."
} else {
full_text
};
@@ -204,3 +206,28 @@ impl Envelope {
Ok(envelope)
}
}
pub async fn extract_contacts(doc: &TantivyDocument) -> BichonResult<HashSet<String>> {
let fields = SchemaTools::envelope_fields();
let mut all_contacts = HashSet::new();
if let Ok(from_val) = extract_string_field(doc, fields.f_from) {
if !from_val.is_empty() {
all_contacts.insert(from_val);
}
}
let multi_fields = [fields.f_to, fields.f_cc, fields.f_bcc];
for field in multi_fields {
if let Ok(vals) = extract_vec_string_field(doc, field) {
for v in vals {
if !v.is_empty() {
all_contacts.insert(v);
}
}
}
}
Ok(all_contacts)
}
+52 -10
View File
@@ -24,7 +24,10 @@ use std::{
time::Duration,
};
use crate::modules::message::{search::SortBy, tags::TagCount};
use crate::modules::{
indexer::envelope::extract_contacts,
message::{search::SortBy, tags::TagCount},
};
use crate::{
modules::{
account::migration::AccountModel,
@@ -57,7 +60,10 @@ use tantivy::{
AggregationCollector, Key,
},
collector::{Count, FacetCollector, TopDocs},
query::{AllQuery, BooleanQuery, EmptyQuery, Occur, Query, QueryParser, RangeQuery, RegexQuery, TermQuery},
query::{
AllQuery, BooleanQuery, EmptyQuery, Occur, Query, QueryParser, RangeQuery, RegexQuery,
TermQuery,
},
schema::{Facet, IndexRecordOption, Value},
store::{Compressor, ZstdCompressor},
DocAddress, Index, IndexBuilder, IndexReader, IndexSettings, IndexWriter, Order,
@@ -307,10 +313,7 @@ impl EnvelopeIndexManager {
] {
if let Some(ref v) = opt_value {
if let Ok(query) = RegexQuery::from_pattern(v.as_str(), field) {
subqueries.push((
Occur::Must,
Box::new(query),
));
subqueries.push((Occur::Must, Box::new(query)));
}
}
}
@@ -329,10 +332,7 @@ impl EnvelopeIndexManager {
if let Some(ref name) = filter.attachment_name {
if let Ok(query) = RegexQuery::from_pattern(name.as_str(), f.f_attachments) {
subqueries.push((
Occur::Must,
Box::new(query),
));
subqueries.push((Occur::Must, Box::new(query)));
}
}
@@ -530,6 +530,48 @@ impl EnvelopeIndexManager {
Ok(all_facets)
}
pub async fn get_all_contacts(
&self,
accounts: Option<HashSet<u64>>,
) -> BichonResult<HashSet<String>> {
let searcher = self.create_searcher()?;
let query: Box<dyn Query> = match accounts {
Some(ref ids) if !ids.is_empty() => {
let mut subqueries = Vec::new();
for &id in ids {
let term =
Term::from_field_u64(SchemaTools::envelope_fields().f_account_id, id);
subqueries.push((
Occur::Should,
Box::new(TermQuery::new(term, IndexRecordOption::Basic)) as Box<dyn Query>,
));
}
Box::new(BooleanQuery::new(subqueries))
}
Some(_) => Box::new(EmptyQuery),
None => Box::new(AllQuery),
};
let mut contacts_set: HashSet<String> = HashSet::new();
let top_docs = searcher
.search(&query, &TopDocs::with_limit(1_000_000))
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
for (_score, doc_address) in top_docs {
let doc: TantivyDocument = searcher
.doc_async(doc_address)
.await
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
let contacts = extract_contacts(&doc).await?;
for value in contacts {
contacts_set.insert(value);
}
}
Ok(contacts_set)
}
pub async fn delete_envelopes_multi_account(
&self,
deletes: &HashMap<u64, Vec<u64>>, // HashMap<account_id, envelope_ids>
+10
View File
@@ -0,0 +1,10 @@
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
pub struct Contact {
pub email: String,
pub name: Option<String>,
}
+1
View File
@@ -17,6 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
pub mod append;
pub mod contacts;
pub mod content;
pub mod delete;
pub mod list;
+21
View File
@@ -323,4 +323,25 @@ impl MessageApi {
.await?;
Ok(())
}
#[oai(
path = "/all-contacts",
method = "get",
operation_id = "get_all_contacts"
)]
async fn get_all_contacts(&self, context: ClientContext) -> ApiResult<Json<HashSet<String>>> {
let authorized_ids: Option<HashSet<u64>> = if context
.has_permission(None, Permission::DATA_READ_ALL)
.await
{
None
} else {
Some(context.user.account_access_map.keys().cloned().collect())
};
Ok(Json(
ENVELOPE_INDEX_MANAGER
.get_all_contacts(authorized_ids)
.await?,
))
}
}