From 6d5953c73b6d8513e82f47915a75d3f25c8cc00b Mon Sep 17 00:00:00 2001 From: rustmailer Date: Fri, 10 Apr 2026 02:08:30 +0800 Subject: [PATCH] adjust the indexing strategy for attachment attributes --- Cargo.lock | 1 + Cargo.toml | 1 + src/modules/logger/mod.rs | 2 + src/modules/message/attachment.rs | 24 +-- src/modules/store/tantivy/manager.rs | 161 +++++++++++------- src/modules/store/tantivy/model.rs | 18 +- src/modules/store/tantivy/schema.rs | 13 +- web/src/api/mailbox/envelope/api.ts | 19 ++- web/src/features/search/account-popover.tsx | 18 ++ web/src/features/search/attachment-filter.tsx | 19 +++ .../search/attachment-metadata-selector.tsx | 53 +++++- web/src/features/search/contact-popover.tsx | 18 ++ web/src/features/search/filter-reset.tsx | 19 +++ web/src/features/search/mailbox-popover.tsx | 19 +++ .../features/search/more-filters-popover.tsx | 18 ++ .../features/search/nested-email-dialog.tsx | 18 ++ .../features/search/tag-filter-popover.tsx | 18 ++ web/src/features/search/text-search-input.tsx | 18 ++ web/src/features/search/time-popover.tsx | 18 ++ .../settings/appearance/appearance-form.tsx | 19 +++ .../features/settings/appearance/index.tsx | 18 ++ 21 files changed, 401 insertions(+), 111 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e7e68a2..0a833cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,6 +549,7 @@ dependencies = [ "toml", "tracing", "tracing-appender", + "tracing-log", "tracing-subscriber", "url", "urlencoding", diff --git a/Cargo.toml b/Cargo.toml index 28e189b..ec35ff6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/modules/logger/mod.rs b/src/modules/logger/mod.rs index a2f95c1..ff63b50 100644 --- a/src/modules/logger/mod.rs +++ b/src/modules/logger/mod.rs @@ -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 { diff --git a/src/modules/message/attachment.rs b/src/modules/message/attachment.rs index 6493f65..b3df9c4 100644 --- a/src/modules/message/attachment.rs +++ b/src/modules/message/attachment.rs @@ -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, + /// 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, - /// A collection of high-level attachment categories. - /// Example: ["document", "image", "archive"] - pub categories: HashSet, + /// 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, - /// A collection of unique MIME types (Content-Type) for the attachments. - /// Example: ["application/pdf", "image/jpeg"] - pub content_types: HashSet, + /// 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, } pub async fn retrieve_attachment_content( diff --git a/src/modules/store/tantivy/manager.rs b/src/modules/store/tantivy/manager.rs index 5494b38..99ebd11 100644 --- a/src/modules/store/tantivy/manager.rs +++ b/src/modules/store/tantivy/manager.rs @@ -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 { 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 = 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" - // ); - // } }); } } diff --git a/src/modules/store/tantivy/model.rs b/src/modules/store/tantivy/model.rs index b077e33..351460d 100644 --- a/src/modules/store/tantivy/model.rs +++ b/src/modules/store/tantivy/model.rs @@ -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); } diff --git a/src/modules/store/tantivy/schema.rs b/src/modules/store/tantivy/schema.rs index 9e7117b..48531ab 100644 --- a/src/modules/store/tantivy/schema.rs +++ b/src/modules/store/tantivy/schema.rs @@ -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); diff --git a/web/src/api/mailbox/envelope/api.ts b/web/src/api/mailbox/envelope/api.ts index f92d44a..df998df 100644 --- a/web/src/api/mailbox/envelope/api.ts +++ b/web/src/api/mailbox/envelope/api.ts @@ -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[]; } diff --git a/web/src/features/search/account-popover.tsx b/web/src/features/search/account-popover.tsx index 6daf83e..7ebb14e 100644 --- a/web/src/features/search/account-popover.tsx +++ b/web/src/features/search/account-popover.tsx @@ -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 . + import * as React from 'react' import { AtSign, ChevronDown, X } from 'lucide-react' import { useTranslation } from 'react-i18next' diff --git a/web/src/features/search/attachment-filter.tsx b/web/src/features/search/attachment-filter.tsx index 51bf5a8..6c9334a 100644 --- a/web/src/features/search/attachment-filter.tsx +++ b/web/src/features/search/attachment-filter.tsx @@ -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 . + + import { Paperclip, Check } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useSearchContext } from './context' diff --git a/web/src/features/search/attachment-metadata-selector.tsx b/web/src/features/search/attachment-metadata-selector.tsx index c6e7531..d980158 100644 --- a/web/src/features/search/attachment-metadata-selector.tsx +++ b/web/src/features/search/attachment-metadata-selector.tsx @@ -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 . + + 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" /> - {isLoading &&
{t('common.loading')}
} - {t('common.noData')} + {isLoading && ( +
+ {t('common.loading')} +
+ )} + + {t('common.noData')} + + {filteredOptions.map((opt) => ( { - 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" > - {opt} - {value === opt && } + {opt.key} + +
+ {/* count */} + + {opt.count} + + + {value === opt.key && ( + + )} +
))}
diff --git a/web/src/features/search/contact-popover.tsx b/web/src/features/search/contact-popover.tsx index 0727244..c07b2cd 100644 --- a/web/src/features/search/contact-popover.tsx +++ b/web/src/features/search/contact-popover.tsx @@ -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 . + import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { useSearchContext } from "./context" import { Button } from "@/components/ui/button" diff --git a/web/src/features/search/filter-reset.tsx b/web/src/features/search/filter-reset.tsx index 9540999..673609f 100644 --- a/web/src/features/search/filter-reset.tsx +++ b/web/src/features/search/filter-reset.tsx @@ -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 . + + import { X } from "lucide-react" import { Button } from "@/components/ui/button" import { useSearchContext } from "./context" diff --git a/web/src/features/search/mailbox-popover.tsx b/web/src/features/search/mailbox-popover.tsx index 00dd2f1..b31f581 100644 --- a/web/src/features/search/mailbox-popover.tsx +++ b/web/src/features/search/mailbox-popover.tsx @@ -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 . + + import * as React from 'react'; import { ChevronDown, Folders, X, TreeDeciduous, FolderIcon, diff --git a/web/src/features/search/more-filters-popover.tsx b/web/src/features/search/more-filters-popover.tsx index 90b7ddb..1fdcdaa 100644 --- a/web/src/features/search/more-filters-popover.tsx +++ b/web/src/features/search/more-filters-popover.tsx @@ -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 . + import * as React from "react" import { Label } from "@/components/ui/label" import { Input } from "@/components/ui/input" diff --git a/web/src/features/search/nested-email-dialog.tsx b/web/src/features/search/nested-email-dialog.tsx index 9a5bde3..74b5462 100644 --- a/web/src/features/search/nested-email-dialog.tsx +++ b/web/src/features/search/nested-email-dialog.tsx @@ -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 . + import { EmailEnvelope } from '@/api'; import { AttachmentInfo, download_nested_attachment, load_nested_message } from '@/api/mailbox/envelope/api'; import EmailIframe from '@/components/mail-iframe'; diff --git a/web/src/features/search/tag-filter-popover.tsx b/web/src/features/search/tag-filter-popover.tsx index af12d08..fdfdd89 100644 --- a/web/src/features/search/tag-filter-popover.tsx +++ b/web/src/features/search/tag-filter-popover.tsx @@ -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 . + import * as React from 'react' import { Tag, ChevronDown, X } from 'lucide-react' import { useTranslation } from 'react-i18next' diff --git a/web/src/features/search/text-search-input.tsx b/web/src/features/search/text-search-input.tsx index 8a7bcc9..ac98ea5 100644 --- a/web/src/features/search/text-search-input.tsx +++ b/web/src/features/search/text-search-input.tsx @@ -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 . + import React, { useState, useEffect, useRef } from "react" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" diff --git a/web/src/features/search/time-popover.tsx b/web/src/features/search/time-popover.tsx index 1206bef..9a040fe 100644 --- a/web/src/features/search/time-popover.tsx +++ b/web/src/features/search/time-popover.tsx @@ -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 . + import * as React from 'react' import { CalendarRange, ChevronDown, X } from 'lucide-react' import { useTranslation } from 'react-i18next' diff --git a/web/src/features/settings/appearance/appearance-form.tsx b/web/src/features/settings/appearance/appearance-form.tsx index 3839d55..a1ac050 100644 --- a/web/src/features/settings/appearance/appearance-form.tsx +++ b/web/src/features/settings/appearance/appearance-form.tsx @@ -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 . + + import { z } from 'zod' import { useForm } from 'react-hook-form' import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons' diff --git a/web/src/features/settings/appearance/index.tsx b/web/src/features/settings/appearance/index.tsx index 208a340..91b11c9 100644 --- a/web/src/features/settings/appearance/index.tsx +++ b/web/src/features/settings/appearance/index.tsx @@ -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 . + import { AppearanceForm } from './appearance-form' export function SettingsAppearance() {