mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
adjust the indexing strategy for attachment attributes
This commit is contained in:
Generated
+1
@@ -549,6 +549,7 @@ dependencies = [
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"urlencoding",
|
||||
|
||||
@@ -124,6 +124,7 @@ blake3 = "1.8.4"
|
||||
uuid = { version = "1.23.0", features = ["v4", "serde"] }
|
||||
fjall = { version = "3.1.3", features = ["lz4", "metrics", "bytes_1"] }
|
||||
tantivy = { version = "0.26.0", features = ["zstd-compression"] }
|
||||
tracing-log = "0.2.0"
|
||||
[dev-dependencies]
|
||||
#bincode = "1.3.3"
|
||||
#secret-lib = "1.0.0"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
use crate::modules::logger::file::setup_file_logger;
|
||||
use crate::modules::settings::cli::SETTINGS;
|
||||
use chrono::Local;
|
||||
use tracing_log::LogTracer;
|
||||
use std::process;
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
|
||||
@@ -34,6 +35,7 @@ impl FormatTime for LocalTimer {
|
||||
}
|
||||
|
||||
pub fn initialize_logging() {
|
||||
LogTracer::init().unwrap();
|
||||
if SETTINGS.bichon_log_to_file {
|
||||
setup_file_logger().unwrap();
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::{collections::HashSet, io::Cursor};
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::{
|
||||
modules::{
|
||||
dashboard::Group,
|
||||
envelope::extractor::reattach_eml_content,
|
||||
error::{code::ErrorCode, BichonResult},
|
||||
utils::compute_content_hash,
|
||||
@@ -15,17 +16,20 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
|
||||
pub struct AttachmentMetadata {
|
||||
/// A collection of unique file extensions found in attachments.
|
||||
/// Example: ["pdf", "docx", "png"]
|
||||
pub extensions: HashSet<String>,
|
||||
/// Statistics of attachment file extensions (key + count).
|
||||
/// Each item represents a file extension and its occurrence count.
|
||||
/// Example: [{ key: "pdf", count: 10 }, { key: "png", count: 5 }]
|
||||
pub extensions: Vec<Group>,
|
||||
|
||||
/// A collection of high-level attachment categories.
|
||||
/// Example: ["document", "image", "archive"]
|
||||
pub categories: HashSet<String>,
|
||||
/// Statistics of attachment categories (key + count).
|
||||
/// Each item represents a high-level category and its occurrence count.
|
||||
/// Example: [{ key: "document", count: 8 }, { key: "image", count: 6 }]
|
||||
pub categories: Vec<Group>,
|
||||
|
||||
/// A collection of unique MIME types (Content-Type) for the attachments.
|
||||
/// Example: ["application/pdf", "image/jpeg"]
|
||||
pub content_types: HashSet<String>,
|
||||
/// Statistics of attachment MIME types (Content-Type) (key + count).
|
||||
/// Each item represents a MIME type and its occurrence count.
|
||||
/// Example: [{ key: "application/pdf", count: 10 }, { key: "image/jpeg", count: 5 }]
|
||||
pub content_types: Vec<Group>,
|
||||
}
|
||||
|
||||
pub async fn retrieve_attachment_content(
|
||||
|
||||
@@ -126,36 +126,54 @@ impl IndexManager {
|
||||
let handler = task::spawn(async move {
|
||||
let mut shutdown = SIGNAL_MANAGER.subscribe();
|
||||
let mut commit_interval = tokio::time::interval(Duration::from_secs(30));
|
||||
let mut pending_count = 0;
|
||||
let commit_threshold = 1000;
|
||||
loop {
|
||||
tokio::select! {
|
||||
maybe_msg = receiver.recv() => {
|
||||
match maybe_msg {
|
||||
Some(doc) => {
|
||||
let writer = writer.lock().await;
|
||||
let mut writer = writer.lock().await;
|
||||
if let Err(e) = writer.add_document(doc) {
|
||||
eprintln!("[ERROR] Failed to add document: {e:?}");
|
||||
tracing::error!("Tantivy: Failed to add document: {e:?}");
|
||||
}
|
||||
pending_count += 1;
|
||||
while let Ok(next_doc) = receiver.try_recv() {
|
||||
let _ = writer.add_document(next_doc);
|
||||
pending_count += 1;
|
||||
}
|
||||
if pending_count >= commit_threshold {
|
||||
tracing::info!("Tantivy: Reached threshold ({}), committing...", pending_count);
|
||||
fatal_commit(&mut writer);
|
||||
pending_count = 0;
|
||||
commit_interval.reset();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
tracing::info!("Tantivy: Receiver closed. Finalizing...");
|
||||
let mut writer = writer.lock().await;
|
||||
fatal_commit(&mut writer);
|
||||
if pending_count > 0 {
|
||||
let mut writer = writer.lock().await;
|
||||
fatal_commit(&mut writer);
|
||||
}
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
_ = commit_interval.tick() => {
|
||||
let mut writer = writer.lock().await;
|
||||
fatal_commit(&mut writer);
|
||||
if pending_count > 0 {
|
||||
let mut writer = writer.lock().await;
|
||||
fatal_commit(&mut writer);
|
||||
pending_count = 0;
|
||||
tracing::debug!("Tantivy: Periodic commit finished.");
|
||||
}
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
tracing::info!("Tantivy: Shutdown signal received. Performing final commit...");
|
||||
let mut writer = writer.lock().await;
|
||||
fatal_commit(&mut writer);
|
||||
if pending_count > 0 {
|
||||
let mut writer = writer.lock().await;
|
||||
fatal_commit(&mut writer);
|
||||
}
|
||||
tracing::info!("Tantivy: Shutdown cleanup complete.");
|
||||
break;
|
||||
}
|
||||
@@ -347,22 +365,21 @@ impl IndexManager {
|
||||
}
|
||||
|
||||
if let Some(ref extension) = filter.attachment_extension {
|
||||
if let Ok(query) = RegexQuery::from_pattern(extension.as_str(), f.f_attachment_glue) {
|
||||
subqueries.push((Occur::Must, Box::new(query)));
|
||||
}
|
||||
let term = Term::from_field_text(f.f_attachment_ext, extension);
|
||||
let query = TermQuery::new(term, IndexRecordOption::Basic);
|
||||
subqueries.push((Occur::Must, Box::new(query)));
|
||||
}
|
||||
|
||||
if let Some(ref category) = filter.attachment_category {
|
||||
if let Ok(query) = RegexQuery::from_pattern(category.as_str(), f.f_attachment_glue) {
|
||||
subqueries.push((Occur::Must, Box::new(query)));
|
||||
}
|
||||
let term = Term::from_field_text(f.f_attachment_category, category);
|
||||
let query = TermQuery::new(term, IndexRecordOption::Basic);
|
||||
subqueries.push((Occur::Must, Box::new(query)));
|
||||
}
|
||||
|
||||
if let Some(ref content_type) = filter.attachment_content_type {
|
||||
if let Ok(query) = RegexQuery::from_pattern(content_type.as_str(), f.f_attachment_glue)
|
||||
{
|
||||
subqueries.push((Occur::Must, Box::new(query)));
|
||||
}
|
||||
let term = Term::from_field_text(f.f_attachment_content_type, content_type);
|
||||
let query = TermQuery::new(term, IndexRecordOption::Basic);
|
||||
subqueries.push((Occur::Must, Box::new(query)));
|
||||
}
|
||||
|
||||
let start_bound = if let Some(from) = filter.since {
|
||||
@@ -1168,6 +1185,28 @@ impl IndexManager {
|
||||
) -> BichonResult<AttachmentMetadata> {
|
||||
let searcher = self.create_searcher()?;
|
||||
|
||||
let aggregations: Aggregations = serde_json::from_value(json!({
|
||||
"exts": {
|
||||
"terms": {
|
||||
"field": F_ATTACHMENT_EXT,
|
||||
"size": 1000
|
||||
}
|
||||
},
|
||||
"cats": {
|
||||
"terms": {
|
||||
"field": F_ATTACHMENT_CATEGORY,
|
||||
"size": 1000
|
||||
}
|
||||
},
|
||||
"content_types": {
|
||||
"terms": {
|
||||
"field": F_ATTACHMENT_CONTENT_TYPE,
|
||||
"size": 1000
|
||||
}
|
||||
},
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let query: Box<dyn Query> = match accounts {
|
||||
Some(ref ids) if !ids.is_empty() => {
|
||||
let mut subqueries = Vec::new();
|
||||
@@ -1184,50 +1223,56 @@ impl IndexManager {
|
||||
None => Box::new(AllQuery),
|
||||
};
|
||||
|
||||
// Extension collector
|
||||
let mut ext_collector = FacetCollector::for_field(F_ATTACHMENT_EXT);
|
||||
ext_collector.add_facet("/");
|
||||
|
||||
// Category collector
|
||||
let mut cat_collector = FacetCollector::for_field(F_ATTACHMENT_CATEGORY);
|
||||
cat_collector.add_facet("/");
|
||||
|
||||
// Content-Type collector
|
||||
let mut type_collector = FacetCollector::for_field(F_ATTACHMENT_CONTENT_TYPE);
|
||||
type_collector.add_facet("/");
|
||||
|
||||
let ext_counts = searcher
|
||||
.search(&query, &ext_collector)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
let cat_counts = searcher
|
||||
.search(&query, &cat_collector)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
let type_counts = searcher
|
||||
.search(&query, &type_collector)
|
||||
let agg_collector = AggregationCollector::from_aggs(aggregations, Default::default());
|
||||
let agg_results = searcher
|
||||
.search(&query, &agg_collector)
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||
|
||||
let mut metadata = AttachmentMetadata {
|
||||
extensions: HashSet::new(),
|
||||
categories: HashSet::new(),
|
||||
content_types: HashSet::new(),
|
||||
};
|
||||
|
||||
for (facet, _) in ext_counts.get("/") {
|
||||
let val = facet.to_path_string().trim_start_matches('/').to_string();
|
||||
metadata.extensions.insert(val);
|
||||
let mut exts = Vec::with_capacity(20);
|
||||
let extensions = agg_results.0.get("exts").unwrap();
|
||||
if let AggregationResult::BucketResult(BucketResult::Terms { buckets, .. }) = extensions {
|
||||
for entry in buckets {
|
||||
if let Key::Str(ext) = &entry.key {
|
||||
exts.push(Group {
|
||||
key: ext.clone(),
|
||||
count: entry.doc_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (facet, _) in cat_counts.get("/") {
|
||||
let val = facet.to_path_string().trim_start_matches('/').to_string();
|
||||
metadata.categories.insert(val);
|
||||
let mut cats = Vec::with_capacity(20);
|
||||
let categories = agg_results.0.get("cats").unwrap();
|
||||
if let AggregationResult::BucketResult(BucketResult::Terms { buckets, .. }) = categories {
|
||||
for entry in buckets {
|
||||
if let Key::Str(cat) = &entry.key {
|
||||
cats.push(Group {
|
||||
key: cat.clone(),
|
||||
count: entry.doc_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (facet, _) in type_counts.get("/") {
|
||||
let val = facet.to_path_string().trim_start_matches('/').to_string();
|
||||
metadata.content_types.insert(val);
|
||||
let mut ctypes = Vec::with_capacity(20);
|
||||
let content_types = agg_results.0.get("content_types").unwrap();
|
||||
if let AggregationResult::BucketResult(BucketResult::Terms { buckets, .. }) = content_types
|
||||
{
|
||||
for entry in buckets {
|
||||
if let Key::Str(content_type) = &entry.key {
|
||||
ctypes.push(Group {
|
||||
key: content_type.clone(),
|
||||
count: entry.doc_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
Ok(AttachmentMetadata {
|
||||
extensions: exts,
|
||||
categories: cats,
|
||||
content_types: ctypes,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_dashboard_stats(
|
||||
@@ -1385,16 +1430,6 @@ impl IndexManager {
|
||||
"failed to cleanup envelope index"
|
||||
);
|
||||
}
|
||||
//这里要删除eml和attachments, 要先得到content_hash, 似乎有点麻烦,因为id有点多
|
||||
// if let Err(e) =
|
||||
// INDEX_MANAGER.delete_account_envelopes(account_id).await
|
||||
// {
|
||||
// tracing::error!(
|
||||
// account_id = account_id,
|
||||
// error = %e,
|
||||
// "failed to cleanup eml index"
|
||||
// );
|
||||
// }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use tantivy::{
|
||||
schema::{Facet, Value},
|
||||
TantivyDocument,
|
||||
};
|
||||
use tantivy::{schema::Value, TantivyDocument};
|
||||
|
||||
use crate::{
|
||||
modules::{
|
||||
@@ -66,21 +62,15 @@ impl EnvelopeWithAttachments {
|
||||
|
||||
if let Some(ext) = att.get_extension() {
|
||||
search_terms.push(ext.clone());
|
||||
doc.add_facet(fields.f_attachment_ext, Facet::from(&format!("/{}", ext)));
|
||||
doc.add_text(fields.f_attachment_ext, ext);
|
||||
}
|
||||
let category = att.get_category().to_string();
|
||||
search_terms.push(category.clone());
|
||||
let file_type = att.file_type.to_lowercase();
|
||||
search_terms.push(file_type.clone());
|
||||
|
||||
doc.add_facet(
|
||||
fields.f_attachment_category,
|
||||
Facet::from(&format!("/{}", category)),
|
||||
);
|
||||
doc.add_facet(
|
||||
fields.f_attachment_content_type,
|
||||
Facet::from(&format!("/{}", file_type)),
|
||||
);
|
||||
doc.add_text(fields.f_attachment_category, category);
|
||||
doc.add_text(fields.f_attachment_content_type, file_type);
|
||||
|
||||
doc.add_text(fields.f_attachment_content_hash, &att.content_hash);
|
||||
}
|
||||
|
||||
@@ -84,14 +84,13 @@ impl SchemaTools {
|
||||
let f_attachments = builder.add_text_field(F_ATTACHMENTS, STORED);
|
||||
let f_attachment_content_hash =
|
||||
builder.add_text_field(F_ATTACHMENT_CONTENT_HASH, STRING | FAST | STORED);
|
||||
let f_attachment_ext =
|
||||
builder.add_facet_field(F_ATTACHMENT_EXT, FacetOptions::default().set_stored());
|
||||
|
||||
let f_attachment_ext = builder.add_text_field(F_ATTACHMENT_EXT, STRING | FAST | STORED);
|
||||
let f_attachment_category =
|
||||
builder.add_facet_field(F_ATTACHMENT_CATEGORY, FacetOptions::default().set_stored());
|
||||
let f_attachment_content_type = builder.add_facet_field(
|
||||
F_ATTACHMENT_CONTENT_TYPE,
|
||||
FacetOptions::default().set_stored(),
|
||||
);
|
||||
builder.add_text_field(F_ATTACHMENT_CATEGORY, STRING | FAST | STORED);
|
||||
let f_attachment_content_type =
|
||||
builder.add_text_field(F_ATTACHMENT_CONTENT_TYPE, STRING | FAST | STORED);
|
||||
|
||||
let f_tags = builder.add_facet_field(F_TAGS, FacetOptions::default().set_stored());
|
||||
let f_shard_id = builder.add_u64_field(F_SHARD_ID, INDEXED | STORED | FAST);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { EmailEnvelope, PaginatedResponse } from "@/api";
|
||||
import axiosInstance from "@/api/axiosInstance";
|
||||
import { Group } from "@/api/system/api";
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
export const get_thread_messages = async (accountId: number, thread_id: string, page: number, page_size: number) => {
|
||||
@@ -114,22 +115,22 @@ export const restore_message = async (accountId: number, envelopeIds: string[])
|
||||
|
||||
export interface AttachmentMetadata {
|
||||
/**
|
||||
* A collection of unique file extensions found in attachments.
|
||||
* @example ["pdf", "docx", "png"]
|
||||
* Statistics of attachment file extensions (key + count).
|
||||
* @example [{ key: "pdf", count: 10 }, { key: "png", count: 5 }]
|
||||
*/
|
||||
extensions: string[];
|
||||
extensions: Group[];
|
||||
|
||||
/**
|
||||
* A collection of high-level attachment categories.
|
||||
* @example ["document", "image", "archive"]
|
||||
* Statistics of attachment categories (key + count).
|
||||
* @example [{ key: "document", count: 8 }, { key: "image", count: 6 }]
|
||||
*/
|
||||
categories: string[];
|
||||
categories: Group[];
|
||||
|
||||
/**
|
||||
* A collection of unique MIME types (Content-Type) for the attachments.
|
||||
* @example ["application/pdf", "image/jpeg"]
|
||||
* Statistics of attachment MIME types (Content-Type) (key + count).
|
||||
* @example [{ key: "application/pdf", count: 10 }, { key: "image/jpeg", count: 5 }]
|
||||
*/
|
||||
content_types: string[];
|
||||
content_types: Group[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
import * as React from 'react'
|
||||
import { AtSign, ChevronDown, X } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
|
||||
import { Paperclip, Check } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSearchContext } from './context'
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
|
||||
import * as React from "react"
|
||||
import { Check, X } from "lucide-react"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Group } from "@/api/system/api"
|
||||
|
||||
interface MetadataSelectorFieldProps {
|
||||
label: string
|
||||
value?: string
|
||||
options: string[]
|
||||
options: Group[]
|
||||
isLoading: boolean
|
||||
onSelect: (val: string | undefined) => void
|
||||
onReset: () => void
|
||||
@@ -27,7 +47,7 @@ export function MetadataSelectorField({
|
||||
|
||||
const filteredOptions = React.useMemo(() => {
|
||||
return options.filter(opt =>
|
||||
opt.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
opt.key.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
}, [options, searchTerm])
|
||||
|
||||
@@ -76,19 +96,36 @@ export function MetadataSelectorField({
|
||||
className="h-8"
|
||||
/>
|
||||
<CommandList className="max-h-[240px]">
|
||||
{isLoading && <div className="p-4 text-[10px] text-center opacity-50">{t('common.loading')}</div>}
|
||||
<CommandEmpty className="text-[10px] p-2 text-center">{t('common.noData')}</CommandEmpty>
|
||||
{isLoading && (
|
||||
<div className="p-4 text-[10px] text-center opacity-50">
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
)}
|
||||
<CommandEmpty className="text-[10px] p-2 text-center">
|
||||
{t('common.noData')}
|
||||
</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{filteredOptions.map((opt) => (
|
||||
<CommandItem
|
||||
key={opt}
|
||||
key={opt.key}
|
||||
onSelect={() => {
|
||||
value === opt ? onReset() : onSelect(opt);
|
||||
value === opt.key ? onReset() : onSelect(opt.key)
|
||||
}}
|
||||
className="flex items-center justify-between py-2 px-3 cursor-pointer text-xs"
|
||||
>
|
||||
<span className="truncate">{opt}</span>
|
||||
{value === opt && <Check className="h-3 w-3 text-primary shrink-0" />}
|
||||
<span className="truncate">{opt.key}</span>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* count */}
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{opt.count}
|
||||
</span>
|
||||
|
||||
{value === opt.key && (
|
||||
<Check className="h-3 w-3 text-primary shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { useSearchContext } from "./context"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
|
||||
import { X } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useSearchContext } from "./context"
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ChevronDown, Folders, X, TreeDeciduous, FolderIcon,
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
import * as React from "react"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
import { EmailEnvelope } from '@/api';
|
||||
import { AttachmentInfo, download_nested_attachment, load_nested_message } from '@/api/mailbox/envelope/api';
|
||||
import EmailIframe from '@/components/mail-iframe';
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
import * as React from 'react'
|
||||
import { Tag, ChevronDown, X } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
import * as React from 'react'
|
||||
import { CalendarRange, ChevronDown, X } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
|
||||
import { z } from 'zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons'
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
import { AppearanceForm } from './appearance-form'
|
||||
|
||||
export function SettingsAppearance() {
|
||||
|
||||
Reference in New Issue
Block a user