Compare commits

7 Commits

Author SHA1 Message Date
rustmailer
602bc223d9 feat: add poem MCP server as optional service endpoint 2026-05-18 07:56:23 +08:00
rustmailer
6e984f376c Update Cargo.lock 2026-05-17 12:29:48 +08:00
rustmailer
7470125a23 bump to v1.0.2 2026-05-17 12:29:42 +08:00
rustmailer
469d254e2b fix: can't select folders & scroll issue in Choose Mailboxes #222 #217 2026-05-17 12:26:49 +08:00
rustmailer
7fde7ee19a update 2026-05-17 10:35:32 +08:00
rustmailer
d543508a23 fix: Overviews are breaking out of their boxes on the dashboard (v1.0.0) #218 2026-05-17 10:35:25 +08:00
rustmailer
f440069912 add debug info in bichon-cli #224 2026-05-17 10:35:07 +08:00
37 changed files with 867 additions and 120 deletions

10
Cargo.lock generated
View File

@@ -293,7 +293,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bichon-admin"
version = "1.0.1"
version = "1.0.2"
dependencies = [
"bichon-core",
"console",
@@ -312,7 +312,7 @@ dependencies = [
[[package]]
name = "bichon-cli"
version = "1.0.1"
version = "1.0.2"
dependencies = [
"base64 0.22.1",
"bichon-core",
@@ -338,7 +338,7 @@ dependencies = [
[[package]]
name = "bichon-core"
version = "1.0.1"
version = "1.0.2"
dependencies = [
"async-imap",
"base64 0.22.1",
@@ -396,7 +396,7 @@ dependencies = [
[[package]]
name = "bichon-server"
version = "1.0.1"
version = "1.0.2"
dependencies = [
"bichon-core",
"bichon-smtp",
@@ -421,7 +421,7 @@ dependencies = [
[[package]]
name = "bichon-smtp"
version = "1.0.1"
version = "1.0.2"
dependencies = [
"base64 0.22.1",
"bichon-core",

View File

@@ -11,7 +11,7 @@ members = [
resolver = "2"
[workspace.package]
version = "1.0.1"
version = "1.0.2"
edition = "2021"
[workspace.dependencies]

View File

@@ -43,7 +43,7 @@ async fn run_interactive() {
let main_options = vec![
"Reset Admin Password",
"Migrate Legacy v0.3.7 Storage to v1.0.0",
"Migrate Legacy v0.3.7 Storage to v1.0.x",
"Exit",
];

View File

@@ -11,7 +11,7 @@ use indicatif::{ProgressBar, ProgressStyle};
pub fn handle_migration(theme: &ColorfulTheme) {
println!(
"\n{}",
style("MIGRATION: Bichon v0.3.7 Storage Architecture → v1.0.0")
style("MIGRATION: Bichon v0.3.7 Storage Architecture → v1.0.x")
.bold()
.yellow()
);
@@ -20,7 +20,7 @@ pub fn handle_migration(theme: &ColorfulTheme) {
"{}",
style(
"This tool migrates data from the legacy v0.3.7 Tantivy-based storage \
architecture to the new v1.0.0 \
architecture to the new v1.0.x \
separated index and Fjall-backed storage format."
)
.dim()
@@ -32,7 +32,7 @@ pub fn handle_migration(theme: &ColorfulTheme) {
"Legacy v0.3.7 architecture:\n\
• envelope metadata stored in Tantivy\n\
• message data stored in Tantivy\n\n\
New v1.0.0 architecture:\n\
New v1.0.x architecture:\n\
• mail indexes stored in Tantivy\n\
• attachment indexes stored in Tantivy\n\
• raw message data stored in Fjall\n\
@@ -172,7 +172,7 @@ pub fn handle_migration(theme: &ColorfulTheme) {
println!(
"{} {}",
style("").green(),
style("Legacy v0.x Tantivy-based storage detected. Migration to v1.0 is required.")
style("Legacy v0.3.7 Tantivy-based storage detected. Migration to v1.0 is required.")
.yellow()
);
}
@@ -180,7 +180,7 @@ pub fn handle_migration(theme: &ColorfulTheme) {
println!(
"{} {}",
style("").green(),
style("No legacy v0.x storage layout was detected at the specified paths.").green()
style("No legacy v0.3.7 storage layout was detected at the specified paths.").green()
);
println!(

View File

@@ -27,19 +27,17 @@ use bichon_core::{
users::{permissions::Permission, view::UserView},
};
use crate::BichonCliConfig as BichonCliConfig;
pub async fn verify_user_and_get_account(
config: &BichonCliConfig,
theme: &ColorfulTheme,
only_nosync: bool,
) -> MinimalAccount {
let client = Client::new();
let url = format!("{}/api/v1/current-user", config.base_url);
use crate::BichonCliConfig;
async fn fetch_json<T: serde::de::DeserializeOwned>(
client: &Client,
url: &str,
token: &str,
label: &str,
) -> T {
let response = match client
.get(&url)
.header("Authorization", format!("Bearer {}", config.api_token))
.get(url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
{
@@ -59,19 +57,15 @@ pub async fn verify_user_and_get_account(
}
};
if !response.status().is_success() {
let status = response.status();
let error_body = response
.text()
.await
.unwrap_or_else(|_| "No error detail provided".to_string());
let status = response.status();
let body = response.text().await.unwrap_or_else(|_| String::new());
if !status.is_success() {
eprintln!(
"\n{} Server returned an error (Status: {})",
style("✘ API Error:").red().bold(),
style(status).yellow()
);
if status == 401 {
eprintln!(
"{} Your API Token seems to be invalid or expired.",
@@ -83,36 +77,66 @@ pub async fn verify_user_and_get_account(
style("Context:").dim()
);
}
eprintln!("{} {}", style("Response:").dim(), error_body);
eprintln!("{} {}", style("Response:").dim(), body);
process::exit(1);
}
let user: UserView = response.json().await.expect("Failed to parse user data");
println!("Welcome, {}!", style(&user.username).cyan());
let account_list_url = format!(
"{}/api/v1/minimal-account-list?only_nosync={only_nosync}",
config.base_url
);
let acc_response = client
.get(&account_list_url)
.header("Authorization", format!("Bearer {}", config.api_token))
.send()
.await
.expect("Failed to fetch account list");
if !acc_response.status().is_success() {
panic!(
"Failed to retrieve accounts. Status: {}",
acc_response.status()
if body.is_empty() {
eprintln!(
"\n{} Server returned an empty response for [{}] (Status: {})",
style("✘ Empty Response:").red().bold(),
label,
status
);
eprintln!(
"{} This may be caused by a reverse proxy or middleware issue.",
style("Tip:").cyan()
);
process::exit(1);
}
let accounts: Vec<MinimalAccount> = acc_response
.json()
.await
.expect("Failed to parse minimal account list");
match serde_json::from_str::<T>(&body) {
Ok(data) => data,
Err(e) => {
eprintln!(
"\n{} Failed to parse response for [{}]: {}",
style("✘ Parse Error:").red().bold(),
label,
e
);
eprintln!("{} Raw body: {}", style("Debug:").dim(), body);
process::exit(1);
}
}
}
pub async fn verify_user_and_get_account(
config: &BichonCliConfig,
theme: &ColorfulTheme,
only_nosync: bool,
) -> MinimalAccount {
let client = Client::new();
let user: UserView = fetch_json(
&client,
&format!("{}/api/v1/current-user", config.base_url),
&config.api_token,
"current-user",
)
.await;
println!("Welcome, {}!", style(&user.username).cyan());
let accounts: Vec<MinimalAccount> = fetch_json(
&client,
&format!(
"{}/api/v1/minimal-account-list?only_nosync={only_nosync}",
config.base_url
),
&config.api_token,
"minimal-account-list",
)
.await;
if accounts.is_empty() {
println!(
@@ -129,6 +153,7 @@ pub async fn verify_user_and_get_account(
);
process::exit(1);
}
let required_permission = Permission::DATA_IMPORT_BATCH;
let mut selectable_accounts = Vec::new();
let mut options = Vec::new();

View File

@@ -13,10 +13,7 @@ use fjall::{
CompressionType, Database, Keyspace, KeyspaceCreateOptions, KvSeparationOptions,
};
use mail_parser::MessageParser;
use tantivy::{
indexer::{LogMergePolicy, NoMergePolicy},
Index, IndexWriter, TantivyDocument,
};
use tantivy::{indexer::NoMergePolicy, Index, IndexWriter, TantivyDocument};
use uuid::Uuid;
use crate::{

View File

@@ -250,6 +250,14 @@ pub struct Settings {
)]
pub bichon_enable_smtp: bool,
#[clap(
long,
env,
default_value = "false",
help = "Enable the MCP (Model Context Protocol) server for AI assistant integration"
)]
pub bichon_enable_mcp: bool,
#[clap(
long,
env,

View File

@@ -54,6 +54,7 @@ pub struct SystemConfigurations {
pub bichon_index_dir: Option<String>,
pub bichon_data_dir: Option<String>,
pub bichon_enable_mcp: bool,
pub bichon_enable_smtp: bool,
pub bichon_smtp_port: u16,
pub bichon_smtp_encryption: String,
@@ -88,6 +89,7 @@ impl From<&Settings> for SystemConfigurations {
bichon_base_url: s.bichon_base_url.clone(),
bichon_index_dir: s.bichon_index_dir.clone(),
bichon_data_dir: s.bichon_data_dir.clone(),
bichon_enable_mcp: s.bichon_enable_mcp,
bichon_enable_smtp: s.bichon_enable_smtp,
bichon_smtp_port: s.bichon_smtp_port,
bichon_smtp_encryption: s.bichon_smtp_encryption.to_string(),

View File

@@ -30,6 +30,8 @@ tokio.workspace = true
http.workspace = true
urlencoding.workspace = true
mimalloc.workspace = true
poem-mcpserver = { version = "0.3", features = ["streamable-http"] }
schemars = "1.0"
[dev-dependencies]
poem = { version = "3.1.12", features = ["test"] }

View File

@@ -46,6 +46,7 @@ use crate::rest::start_http_server;
pub mod common;
pub mod error;
pub mod mcp;
pub mod rest;
#[global_allocator]

View File

@@ -0,0 +1,43 @@
//
// 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/>.
use bichon_core::common::auth::ClientContext;
use poem::Request;
use poem_mcpserver::{streamable_http, McpServer};
mod tools;
use tools::BichonMcpTools;
/// Create a Poem endpoint that handles MCP Streamable HTTP requests.
///
/// The endpoint requires authentication via `ApiGuard` middleware (applied at
/// the route level in `rest/mod.rs`). The guard stores a `ClientContext` in
/// request extensions, which the server factory reads to configure per-session
/// tool authorization.
pub fn mcp_endpoint() -> impl poem::IntoEndpoint {
streamable_http::endpoint(|req: &Request| {
let ctx = req
.extensions()
.get::<ClientContext>()
.expect("ApiGuard middleware must provide ClientContext in request extensions")
.clone();
McpServer::new()
.with_server_info("bichon-mcp", env!("CARGO_PKG_VERSION"))
.tools(BichonMcpTools::new(ctx))
})
}

View File

@@ -0,0 +1,318 @@
//
// 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/>.
use std::collections::HashSet;
use bichon_core::{
account::migration::AccountModel,
common::auth::ClientContext,
dashboard::DashboardStats,
message::search::{
search_attachment_impl, search_messages_impl, AttachmentSearchFilter, AttachmentSearchRequest,
EmailSearchFilter, EmailSearchRequest, SortBy,
},
message::{content::retrieve_email_content, list::get_thread_messages},
users::permissions::Permission,
};
use poem_mcpserver::{content::Text, Tools};
/// Tools for interacting with the Bichon email archive via MCP.
///
/// Provides email search, content retrieval, thread viewing, attachment search,
/// dashboard statistics, and account management capabilities.
pub struct BichonMcpTools {
context: ClientContext,
}
impl BichonMcpTools {
pub fn new(context: ClientContext) -> Self {
Self { context }
}
/// Derive the set of account IDs that the current user is authorized to access.
/// Returns None if the user has global access (DATA_READ_ALL or ACCOUNT_MANAGE_ALL),
/// meaning "all accounts are accessible".
fn authorized_accounts(&self) -> Option<HashSet<u64>> {
if self.context.has_permission(None, Permission::DATA_READ_ALL)
|| self.context.has_permission(None, Permission::ACCOUNT_MANAGE_ALL)
{
None
} else {
Some(
self.context
.user
.account_access_map
.keys()
.copied()
.collect(),
)
}
}
/// Restrict the given account_ids to only those the user is authorized to access.
/// If the user has global access, the input is returned as-is.
fn restrict_account_ids(
&self,
account_ids: Option<Vec<u64>>,
) -> Option<HashSet<u64>> {
let global = self.authorized_accounts();
match (global, account_ids) {
// User has global access → return input as HashSet (or None)
(None, Some(ids)) => Some(ids.into_iter().collect()),
(None, None) => None,
// User is scoped → intersect with authorized set
(Some(authorized), Some(ids)) => {
let filtered: HashSet<u64> =
ids.into_iter().filter(|id| authorized.contains(id)).collect();
if filtered.is_empty() {
Some(filtered)
} else {
Some(filtered)
}
}
(Some(authorized), None) => Some(authorized),
}
}
}
#[Tools]
impl BichonMcpTools {
/// Search archived emails using full-text and structured filters.
async fn search_emails(
&self,
/// Full-text search across all message fields (subject, body, from, to, etc.)
text: Option<String>,
/// Filter by email subject line
subject: Option<String>,
/// Filter by sender email address
from: Option<String>,
/// Filter by recipient email address
to: Option<String>,
/// Start date as Unix timestamp in milliseconds
since: Option<i64>,
/// End date as Unix timestamp in milliseconds
before: Option<i64>,
/// Filter by specific account IDs
account_ids: Option<Vec<u64>>,
/// Filter by specific mailbox/folder IDs
mailbox_ids: Option<Vec<u64>>,
/// Only return messages that have attachments
has_attachment: Option<bool>,
/// Filter by attachment filename
attachment_name: Option<String>,
/// Filter by tags/labels
tags: Option<Vec<String>>,
/// Page number (1-based, default 1)
page: Option<u64>,
/// Results per page (default 50, max 500)
page_size: Option<u64>,
) -> Result<Text<String>, String> {
let authorized_ids = self.restrict_account_ids(account_ids);
let filter = EmailSearchFilter {
text,
subject,
from,
to,
since,
before,
account_ids: authorized_ids.clone(),
mailbox_ids: mailbox_ids.map(|v| v.into_iter().collect()),
has_attachment,
attachment_name,
tags: tags.map(|v| v.into_iter().collect()),
..Default::default()
};
let request = EmailSearchRequest {
filter,
page: page.unwrap_or(1),
page_size: page_size.unwrap_or(50).min(500),
sort_by: Some(SortBy::DATE),
desc: Some(true),
};
search_messages_impl(authorized_ids, request)
.map(|result| Text(serde_json::to_string_pretty(&result).unwrap_or_default()))
.map_err(|e| format!("Search failed: {:#}", e))
}
/// Retrieve the full content of a specific email including plain text, HTML body,
/// and attachment metadata (filenames, sizes, content hashes).
async fn get_email_content(
&self,
/// The ID of the email account
account_id: u64,
/// The envelope ID of the email message
envelope_id: String,
) -> Result<Text<String>, String> {
self.context
.require_permission(Some(account_id), Permission::DATA_READ)
.map_err(|e| format!("Permission denied: {:#}", e))?;
retrieve_email_content(account_id, envelope_id)
.map(|content| Text(serde_json::to_string_pretty(&content).unwrap_or_default()))
.map_err(|e| format!("Failed to retrieve email content: {:#}", e))
}
/// Retrieve all messages in a specific email thread/conversation.
async fn get_thread(
&self,
/// The ID of the email account
account_id: u64,
/// The thread ID (from the thread_id field of an envelope)
thread_id: String,
/// Page number (1-based, default 1)
page: Option<u64>,
/// Results per page (default 50, max 500)
page_size: Option<u64>,
) -> Result<Text<String>, String> {
self.context
.require_permission(Some(account_id), Permission::DATA_READ)
.map_err(|e| format!("Permission denied: {:#}", e))?;
get_thread_messages(
account_id,
&thread_id,
page.unwrap_or(1),
page_size.unwrap_or(50).min(500),
)
.map(|result| Text(serde_json::to_string_pretty(&result).unwrap_or_default()))
.map_err(|e| format!("Failed to retrieve thread: {:#}", e))
}
/// Search email attachments using filters like filename, extension, content type,
/// category, sender, and date range.
async fn search_attachments(
&self,
/// Full-text search in attachment content and metadata
text: Option<String>,
/// Filter by attachment filename
attachment_name: Option<String>,
/// Filter by file extension (e.g., pdf, docx, jpg)
attachment_extension: Option<String>,
/// Filter by category (document, image, spreadsheet, etc.)
attachment_category: Option<String>,
/// Filter by MIME content type (e.g., application/pdf)
attachment_content_type: Option<String>,
/// Filter by sender email address
from: Option<String>,
/// Start date as Unix timestamp in milliseconds
since: Option<i64>,
/// End date as Unix timestamp in milliseconds
before: Option<i64>,
/// Filter by specific account IDs
account_ids: Option<Vec<u64>>,
/// Minimum attachment size in bytes
min_size: Option<u64>,
/// Maximum attachment size in bytes
max_size: Option<u64>,
/// Page number (1-based, default 1)
page: Option<u64>,
/// Results per page (default 50, max 500)
page_size: Option<u64>,
) -> Result<Text<String>, String> {
let authorized_ids = self.restrict_account_ids(account_ids);
let filter = AttachmentSearchFilter {
text,
attachment_name,
attachment_extension,
attachment_category,
attachment_content_type,
from,
since,
before,
account_ids: authorized_ids.clone(),
min_size,
max_size,
..Default::default()
};
let request: AttachmentSearchRequest =
serde_json::from_value(serde_json::json!({
"filter": filter,
"page": page.unwrap_or(1),
"page_size": page_size.unwrap_or(50).min(500),
"sort_by": "DATE",
"desc": true,
}))
.map_err(|e| format!("Invalid request: {e}"))?;
search_attachment_impl(authorized_ids, request)
.map(|result| Text(serde_json::to_string_pretty(&result).unwrap_or_default()))
.map_err(|e| format!("Attachment search failed: {:#}", e))
}
/// Retrieve summary statistics about the email archive including total emails,
/// attachments, storage usage, top senders, and recent activity.
async fn get_dashboard_stats(&self) -> Result<Text<String>, String> {
if !self.context.has_permission(None, Permission::SYSTEM_ACCESS) {
return Err("Permission denied: requires system:access".into());
}
DashboardStats::get(self.context.clone())
.await
.map(|stats| Text(serde_json::to_string_pretty(&stats).unwrap_or_default()))
.map_err(|e| format!("Failed to retrieve dashboard stats: {:#}", e))
}
/// List all email accounts the current user has access to.
/// Returns minimal account information (ID and email address).
async fn list_accounts(&self) -> Result<Text<String>, String> {
let accounts = AccountModel::minimal_list(false)
.map_err(|e| format!("Failed to list accounts: {:#}", e))?;
let global_access = self
.context
.has_permission(None, Permission::ACCOUNT_MANAGE_ALL);
let visible: Vec<_> = if global_access {
accounts
} else {
let authorized: HashSet<u64> =
self.context.user.account_access_map.keys().copied().collect();
accounts
.into_iter()
.filter(|a| authorized.contains(&a.id))
.collect()
};
Ok(Text(
serde_json::to_string_pretty(&visible).unwrap_or_default(),
))
}
/// Retrieve detailed configuration and status of a specific email account.
async fn get_account(
&self,
/// The ID of the email account
account_id: u64,
) -> Result<Text<String>, String> {
self.context
.require_permission(Some(account_id), Permission::ACCOUNT_READ_DETAILS)
.map_err(|e| format!("Permission denied: {:#}", e))?;
let account = AccountModel::get(account_id)
.map_err(|e| format!("Account not found: {:#}", e))?;
Ok(Text(
serde_json::to_string_pretty(&account).unwrap_or_default(),
))
}
}

View File

@@ -30,6 +30,7 @@ use bichon_core::settings::cli::SETTINGS;
use super::error::ApiErrorResponse;
use crate::common::auth::ApiGuard;
use crate::common::timeout::{Timeout, TIMEOUT_HEADER};
use crate::mcp::mcp_endpoint;
use api::create_openapi_service;
use assets::FrontEndAssets;
use bichon_core::raise_error;
@@ -123,7 +124,20 @@ pub async fn start_http_server() -> BichonResult<()> {
.nest("/api-docs/spec.yaml", spec_yaml)
.nest("/oauth2/callback", get(oauth2_callback))
.nest("/api/status", get(get_status))
.nest("/api/login", post(login))
.nest("/api/login", post(login));
let app_logic = if SETTINGS.bichon_enable_mcp {
app_logic.nest_no_strip(
"/mcp",
mcp_endpoint()
.with(ApiGuard)
.with(Timeout),
)
} else {
app_logic
};
let app_logic = app_logic
.nest_no_strip("/api/v1", open_api_route)
.nest_no_strip(
"/assets",

View File

@@ -0,0 +1,136 @@
// Temporary mock data for download-folders responsive testing
import { MailboxData, MailboxListResponse } from './api';
let _id = 1;
const m = (overrides: Partial<MailboxData>): MailboxData => ({
account_id: 1,
attributes: [],
delimiter: '/',
exists: Math.floor(Math.random() * 5000) + 100,
id: _id++,
name: '',
uid_next: null,
uid_validity: null,
unseen: null,
...overrides,
});
const SHORT = [
m({ name: 'INBOX' }),
m({ name: 'INBOX/Drafts' }),
m({ name: 'INBOX/Sent' }),
m({ name: 'INBOX/Trash' }),
m({ name: 'INBOX/Archive' }),
m({ name: 'INBOX/Spam' }),
m({ name: 'INBOX/Templates' }),
];
const PROJECTS = [
m({ name: 'INBOX/Projects' }),
m({ name: 'INBOX/Projects/Q4 2025 Financial Reports and Annual Budget Planning Review' }),
m({ name: 'INBOX/Projects/Q4 2025 Financial Reports and Annual Budget Planning Review/Drafts' }),
m({
name: 'INBOX/Projects/Q4 2025 Financial Reports and Annual Budget Planning Review/Drafts/Revision 3 - Updated Forecast Models and Department Sign-off Required',
attributes: [{ attr: 'HasChildren', extension: null }],
}),
m({
name: 'INBOX/Projects/Q4 2025 Financial Reports and Annual Budget Planning Review/Drafts/Revision 3 - Updated Forecast Models and Department Sign-off Required/Comments from CFO',
}),
m({
name: 'INBOX/Projects/Q4 2025 Financial Reports and Annual Budget Planning Review/Drafts/Revision 3 - Updated Forecast Models and Department Sign-off Required/Attachments',
}),
m({ name: 'INBOX/Projects/Q4 2025 Financial Reports and Annual Budget Planning Review/Final' }),
m({ name: 'INBOX/Projects/Q4 2025 Financial Reports and Annual Budget Planning Review/Final/Approved with Amendments' }),
m({ name: 'INBOX/Projects/Q1 2026 Strategic Initiative - Cloud Infrastructure Migration Assessment and Vendor Selection' }),
m({ name: 'INBOX/Projects/Q1 2026 Strategic Initiative - Cloud Infrastructure Migration Assessment and Vendor Selection/Kickoff' }),
m({ name: 'INBOX/Projects/Q1 2026 Strategic Initiative - Cloud Infrastructure Migration Assessment and Vendor Selection/Kickoff/Meeting Minutes and Action Items' }),
m({
name: 'INBOX/Projects/Q1 2026 Strategic Initiative - Cloud Infrastructure Migration Assessment and Vendor Selection/Vendor Proposals',
attributes: [{ attr: 'HasNoChildren', extension: null }],
}),
m({ name: 'INBOX/Projects/Q1 2026 Strategic Initiative - Cloud Infrastructure Migration Assessment and Vendor Selection/Vendor Proposals/AWS Proposal Package' }),
m({ name: 'INBOX/Projects/Q1 2026 Strategic Initiative - Cloud Infrastructure Migration Assessment and Vendor Selection/Vendor Proposals/Azure Proposal Package' }),
m({ name: 'INBOX/Projects/Q1 2026 Strategic Initiative - Cloud Infrastructure Migration Assessment and Vendor Selection/Vendor Proposals/GCP Proposal Package' }),
m({ name: 'INBOX/Projects/Q1 2026 Strategic Initiative - Cloud Infrastructure Migration Assessment and Vendor Selection/Internal Review and Scoring Committee' }),
m({ name: 'INBOX/Projects/HR Portal Redesign - Employee Self-Service Platform Modernization' }),
m({ name: 'INBOX/Projects/HR Portal Redesign - Employee Self-Service Platform Modernization/Wireframes and Mockups' }),
m({ name: 'INBOX/Projects/HR Portal Redesign - Employee Self-Service Platform Modernization/Wireframes and Mockups/Iteration 1' }),
m({ name: 'INBOX/Projects/HR Portal Redesign - Employee Self-Service Platform Modernization/Wireframes and Mockups/Iteration 2' }),
m({ name: 'INBOX/Projects/HR Portal Redesign - Employee Self-Service Platform Modernization/Usability Testing Results and Feedback Compilation' }),
];
const CLIENTS = [
m({ name: 'INBOX/Clients' }),
m({
name: 'INBOX/Clients/Acme Corporation - Enterprise Software Licensing and Support Agreement Renewal 2026',
attributes: [{ attr: 'HasChildren', extension: null }],
}),
m({ name: 'INBOX/Clients/Acme Corporation - Enterprise Software Licensing and Support Agreement Renewal 2026/Contract Documents' }),
m({ name: 'INBOX/Clients/Acme Corporation - Enterprise Software Licensing and Support Agreement Renewal 2026/Contract Documents/Redlined Versions' }),
m({
name: 'INBOX/Clients/Acme Corporation - Enterprise Software Licensing and Support Agreement Renewal 2026/Contract Documents/Redlined Versions/Legal Review Round 1',
}),
m({
name: 'INBOX/Clients/Acme Corporation - Enterprise Software Licensing and Support Agreement Renewal 2026/Contract Documents/Redlined Versions/Legal Review Round 2 - Final',
}),
m({
name: 'INBOX/Clients/Acme Corporation - Enterprise Software Licensing and Support Agreement Renewal 2026/Invoices and Payment Records',
}),
m({ name: 'INBOX/Clients/Acme Corporation - Enterprise Software Licensing and Support Agreement Renewal 2026/Support Tickets and Correspondence' }),
m({
name: 'INBOX/Clients/Globex Industries - Multi-Year Digital Transformation Consulting Engagement',
attributes: [{ attr: 'HasChildren', extension: null }],
}),
m({ name: 'INBOX/Clients/Globex Industries - Multi-Year Digital Transformation Consulting Engagement/Phase 1 Discovery and Assessment' }),
m({ name: 'INBOX/Clients/Globex Industries - Multi-Year Digital Transformation Consulting Engagement/Phase 1 Discovery and Assessment/Stakeholder Interviews' }),
m({ name: 'INBOX/Clients/Globex Industries - Multi-Year Digital Transformation Consulting Engagement/Phase 1 Discovery and Assessment/Current State Architecture Documentation' }),
m({ name: 'INBOX/Clients/Globex Industries - Multi-Year Digital Transformation Consulting Engagement/Phase 2 Implementation Roadmap' }),
m({ name: 'INBOX/Clients/Globex Industries - Multi-Year Digital Transformation Consulting Engagement/Phase 2 Implementation Roadmap/Sprint Planning and Resource Allocation' }),
m({ name: 'INBOX/Clients/Globex Industries - Multi-Year Digital Transformation Consulting Engagement/Phase 2 Implementation Roadmap/Risk Assessment and Mitigation Strategies' }),
m({ name: 'INBOX/Clients/Initech Solutions - Cybersecurity Audit and Compliance Remediation Program 2026' }),
m({ name: 'INBOX/Clients/Initech Solutions - Cybersecurity Audit and Compliance Remediation Program 2026/Penetration Testing Reports' }),
m({ name: 'INBOX/Clients/Initech Solutions - Cybersecurity Audit and Compliance Remediation Program 2026/Penetration Testing Reports/External Network Assessment' }),
m({ name: 'INBOX/Clients/Initech Solutions - Cybersecurity Audit and Compliance Remediation Program 2026/Penetration Testing Reports/Internal Network Assessment' }),
m({ name: 'INBOX/Clients/Initech Solutions - Cybersecurity Audit and Compliance Remediation Program 2026/Penetration Testing Reports/Web Application Security Scan Results' }),
m({ name: 'INBOX/Clients/Initech Solutions - Cybersecurity Audit and Compliance Remediation Program 2026/Compliance Gap Analysis and Remediation Tracking' }),
m({ name: 'INBOX/Clients/Massive Dynamic - Research Collaboration on Advanced Machine Learning Applications in Healthcare Informatics' }),
m({ name: 'INBOX/Clients/Massive Dynamic - Research Collaboration on Advanced Machine Learning Applications in Healthcare Informatics/Data Sharing Agreements and Ethics Board Approvals' }),
m({ name: 'INBOX/Clients/Massive Dynamic - Research Collaboration on Advanced Machine Learning Applications in Healthcare Informatics/Literature Review and Prior Art Analysis' }),
m({ name: 'INBOX/Clients/Massive Dynamic - Research Collaboration on Advanced Machine Learning Applications in Healthcare Informatics/Model Training Datasets and Validation Results' }),
];
const NOTIFICATIONS = [
m({ name: 'INBOX/Notifications' }),
m({ name: 'INBOX/Notifications/GitHub Enterprise - Pull Request Reviews and CI/CD Pipeline Status Updates' }),
m({ name: 'INBOX/Notifications/Jira Service Management - Incident Response Alerts and Escalation Notifications' }),
m({ name: 'INBOX/Notifications/Confluence - Documentation Updates and Page Modification Summaries' }),
m({ name: 'INBOX/Notifications/Slack Workspace - Channel Highlights and Direct Message Digest Compilation' }),
m({ name: 'INBOX/Notifications/Datadog - Application Performance Monitoring Alerts and Anomaly Detection Reports' }),
m({ name: 'INBOX/Notifications/PagerDuty - On-Call Rotation Schedule and Incident Acknowledgment Confirmations' }),
m({ name: 'INBOX/Notifications/Microsoft 365 - Calendar Invitations and Meeting Room Booking Confirmations' }),
];
const NEWSLETTERS = [
m({ name: 'INBOX/Newsletters' }),
m({ name: 'INBOX/Newsletters/Rust Weekly - Community Updates, Crate Highlights, and RFC Progress Tracking Digest' }),
m({
name: 'INBOX/Newsletters/Systems Programming Insider - Deep Dive Articles on Memory Management and Concurrency Patterns',
attributes: [{ attr: 'HasNoChildren', extension: null }],
}),
m({ name: 'INBOX/Newsletters/Cloud Native Computing Foundation - Kubernetes Ecosystem Updates and Project Maturity Reports' }),
m({ name: 'INBOX/Newsletters/Software Architecture Monthly - Case Studies in Distributed Systems Design and Microservices Patterns' }),
m({ name: 'INBOX/Newsletters/DevOps Weekly Digest - Tool Reviews, Pipeline Optimization Techniques, and Platform Engineering Insights' }),
m({ name: 'INBOX/Newsletters/Information Security Briefing - CVE Disclosures, Threat Intelligence Reports, and Zero-Day Advisories' }),
m({ name: 'INBOX/Newsletters/Tech Leadership Forum - Engineering Management Best Practices and Organizational Scaling Strategies' }),
];
export const MOCK_MAILBOX_LIST: MailboxListResponse = {
status: 'ready',
mailboxes: [
...SHORT,
...PROJECTS,
...CLIENTS,
...NOTIFICATIONS,
...NEWSLETTERS,
],
};

View File

@@ -138,6 +138,8 @@ export type ServerConfigurations = {
bichon_smtp_auth_required: boolean
bichon_smtp_tls_key_path?: string | null
bichon_smtp_tls_cert_path?: string | null
bichon_enable_mcp: boolean
}
export const get_dashboard_stats = async () => {

View File

@@ -0,0 +1,104 @@
// Temporary mock data for dashboard responsive testing
import { DashboardStats } from './api';
const NOW = Date.now();
const DAY = 86400000;
export const MOCK_DASHBOARD_STATS: DashboardStats = {
account_count: 12,
email_count: 145892,
attachment_count: 34201,
total_size_bytes: 128_849_018_880, // ~120 GB logical
storage_usage_bytes: 85_899_345_920, // ~80 GB blob
index_usage_bytes: 12_884_901_888, // ~12 GB index
recent_activity: Array.from({ length: 30 }, (_, i) => ({
timestamp_ms: NOW - (29 - i) * DAY,
count: Math.floor(Math.random() * 2000) + 200,
})),
top_senders: [
{ key: 'alexander.hamilton@verylongemaildomain-truncation-test.com', count: 4523 },
{ key: 'noreply@github-enterprise-notifications.system.example.org', count: 3891 },
{ key: 'jane.doe+project-alpha-beta-gamma@company-with-long-name.io', count: 3102 },
{ key: 'newsletter-subscriptions@really-long-marketing-domain.co.uk', count: 2845 },
{ key: 'support-tickets+priority-high@helpdesk.corporate.example.com', count: 2100 },
{ key: 'bot-pipeline-ci-cd-failures@devops.internal.long-subdomain.net', count: 1789 },
{ key: 'short@s.dev', count: 1500 },
{ key: 'alerts-monitoring-productions-east-us@observability-platform.com', count: 1256 },
{ key: 'weekly-digest-no-reply@newsletter.huge-media-conglomerate.org', count: 980 },
{ key: 'invitations-events-calendar-reminders@social-network-app.io', count: 760 },
],
top_accounts: [
{ key: 'primary.work.mailbox@enterprise-long-domain-name.com', count: 78500 },
{ key: 'personal.archive+all@very-lengthy-personal-domain.me', count: 42300 },
{ key: 'secondary.backup@another-extremely-long-domain.co', count: 15100 },
{ key: 'team-leads@department-of-engineering.corp.example.org', count: 8992 },
{ key: 'short@x.co', count: 1000 },
],
with_attachment_count: 18500,
without_attachment_count: 15701,
top_largest_emails: [
{
id: 'msg-001',
subject: 'RE: [EXTERNAL] Q4 Financial Reports & Budget Planning Documents for Review - Please Provide Feedback by EOD Friday with Department Head Sign-off Required',
size_bytes: 52_428_800,
},
{
id: 'msg-002',
subject: 'Fwd: Urgent: Client Presentation Draft - Version 7 Final (With Legal Team Amendments and Compliance Review Attached)',
size_bytes: 48_234_496,
},
{
id: 'msg-003',
subject: 'Meeting Minutes: Cross-Functional Architecture Review Session - Microservices Migration Strategy and Timeline Discussion (Part 3 of 5)',
size_bytes: 41_943_040,
},
{
id: 'msg-004',
subject: 'Invoice #INV-2026-04582 - Professional Services Engagement: Cloud Infrastructure Assessment and Remediation Planning Phase II Deliverables',
size_bytes: 38_797_312,
},
{
id: 'msg-005',
subject: '[ACTION REQUIRED] Security Incident Response: Post-Mortem Analysis and Remediation Steps for CVE-2026-12345 - Department-Wide Mandatory Review',
size_bytes: 35_651_584,
},
{
id: 'msg-006',
subject: 'Monthly Newsletter: Engineering Blog Digest - Articles on Distributed Systems, Rust Async Runtime Internals, and Performance Optimization Techniques',
size_bytes: 31_457_280,
},
{
id: 'msg-007',
subject: 'Contract Review: Master Service Agreement Amendment #7 with Third-Party Vendor Integration Services for Payment Processing Platform',
size_bytes: 28_311_552,
},
{
id: 'msg-008',
subject: 'Travel Itinerary & Expense Report: International Conference on Systems Programming - Accommodation, Flight, and Per Diem Documentation Package',
size_bytes: 25_165_824,
},
{
id: 'msg-009',
subject: 'Re: [INTERNAL] Employee Onboarding Documentation Package - Benefits Enrollment, Tax Forms, Direct Deposit Setup, and IT Access Request Forms Bundle',
size_bytes: 22_020_096,
},
{
id: 'msg-010',
subject: 'Data Export Request: Complete Transaction History 2024-2026 with Audit Trail and Compliance Certification for External Regulatory Review Board',
size_bytes: 18_874_368,
},
],
top_largest_attachments: [
{ id: 'att-001', name: 'Q4_2025_Financial_Statements_Audited_with_Supporting_Schedules_and_Notes_v3_FINAL.xlsx', size_bytes: 45_254_100 },
{ id: 'att-002', name: 'project_deliverables_package_phase_2_with_test_results_coverage_report_and_deployment_guide.zip', size_bytes: 38_900_500 },
{ id: 'att-003', name: '2026-01-15_production_database_backup_full_with_transaction_logs_and_stored_procedures.sql.gz', size_bytes: 35_200_000 },
{ id: 'att-004', name: 'client_presentation_deck_v7_final_approved_with_speaker_notes_and_embedded_video_demo.pptx', size_bytes: 31_000_000 },
{ id: 'att-005', name: 'system_architecture_diagrams_microservices_v2_with_sequence_flows_and_deployment_topology.pdf', size_bytes: 28_500_000 },
{ id: 'att-006', name: 'annual_company_event_photograph_high_resolution_group_photo_panorama_2026.jpg', size_bytes: 25_000_000 },
{ id: 'att-007', name: 'complete_source_code_archive_feature_branch_refactor_auth_module_2026_01_15.tar.gz', size_bytes: 22_800_000 },
{ id: 'att-008', name: 'product_demo_screencast_walkthrough_new_features_2026_release_candidate.mp4', size_bytes: 19_500_000 },
{ id: 'att-009', name: 'legal_contract_review_package_with_redlined_amendments_and_counsel_opinion_letters.pdf', size_bytes: 16_200_000 },
{ id: 'att-010', name: 'employee_training_module_compliance_and_security_awareness_2026_v2_interactive.iso', size_bytes: 12_800_000 },
],
system_version: '1.0.1',
};

View File

@@ -31,13 +31,14 @@ import { Loader2, CheckSquare, Square } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { toast } from '@/hooks/use-toast'
import { list_mailboxes, MailboxData } from '@/api/mailbox/api'
//import { MOCK_MAILBOX_LIST } from '@/api/mailbox/mock-mailboxes'
import { buildTree, ExtendedTreeItemProps } from '@/lib/build-tree'
import { Skeleton } from '@/components/ui/skeleton'
import { AccountModel, update_account } from '@/api/account/api'
import { ToastAction } from '@/components/ui/toast'
import axios, { AxiosError } from 'axios'
import { ScrollArea } from '@/components/ui/scroll-area'
import { useTranslation } from 'react-i18next'
import { ScrollArea } from '@/components/ui/scroll-area'
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
import { useTheme } from '@/context/theme-context'
import React from 'react'
@@ -108,7 +109,7 @@ function CustomLabel({
</div>
{exists !== undefined && (
<span
className="text-sm opacity-60 min-w-[40px] text-right text-inherit"
className="text-sm opacity-60 min-w-[40px] text-right text-inherit mr-4"
>
{exists}
</span>
@@ -181,7 +182,7 @@ export function DownloadFoldersDialog({ currentRow, open, onOpenChange }: Props)
setItemsWithChildren(itemsWithChildren);
setExpandedItems(itemsWithChildren);
const download_folders = data
.filter(mailbox => currentRow.download_folders.includes(mailbox.name))
.filter(mailbox => currentRow.download_folders?.includes(mailbox.name))
.map(mailbox => mailbox.id.toString());
setSelectedItems(download_folders);
};
@@ -404,7 +405,7 @@ export function DownloadFoldersDialog({ currentRow, open, onOpenChange }: Props)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-3xl max-h-[90vh] flex flex-col">
<DialogContent className="max-w-[95vw] sm:max-w-3xl max-h-[90vh] flex flex-col overflow-hidden">
<DialogHeader className="flex-shrink-0">
<DialogTitle>{t('accounts.selectMailboxes')}</DialogTitle>
<DialogDescription>
@@ -412,8 +413,8 @@ export function DownloadFoldersDialog({ currentRow, open, onOpenChange }: Props)
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex flex-col pt-2 gap-2">
<div className="flex-1 min-h-0 grid gap-4" style={{ gridTemplateRows: 'auto 1fr' }}>
<div className="flex flex-col gap-2">
<div className="flex gap-2 flex-wrap">
<Button
variant="outline"
@@ -479,7 +480,7 @@ export function DownloadFoldersDialog({ currentRow, open, onOpenChange }: Props)
</div>
</div>
<ScrollArea className="h-[32rem] flex-1 min-h-0 w-full pr-4 -mr-4 py-1">
<ScrollArea className="min-h-0 w-full py-1">
{isLoading && (
<div className="p-8 space-y-8">
<div className="flex flex-col items-center gap-3 text-muted-foreground">

View File

@@ -17,9 +17,11 @@ import { Mail, Users, Inbox, Zap, Paperclip } from 'lucide-react';
import { formatBytes, formatNumber } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query';
import { get_dashboard_stats, INITIAL_DASHBOARD_STATS, TimeBucket } from '@/api/system/api';
//import { MOCK_DASHBOARD_STATS } from '@/api/system/mock-dashboard';
import { Main } from '@/components/layout/main';
import { FixedHeader } from '@/components/layout/fixed-header';
import { useTranslation } from 'react-i18next';
import LongText from '@/components/long-text';
import { getToken } from '@/stores/authStore';
import { useNavigate } from '@tanstack/react-router';
import useMinimalAccountList from '@/hooks/use-minimal-account-list';
@@ -346,18 +348,18 @@ export default function MailArchiveDashboard() {
</CardContent>
</Card>
</div>
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
<Card>
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<Card className="overflow-hidden">
<CardHeader className="!px-4 !pt-4 !pb-1">
<CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.top10Senders')}</CardTitle>
</CardHeader>
<CardContent className="p-0">
{hasTopSenders ? (
<Table>
<Table className="table-fixed">
<TableHeader>
<TableRow>
<TableHead className="text-xs">{t('dashboard.sender')}</TableHead>
<TableHead className="text-right text-xs">{t('dashboard.count')}</TableHead>
<TableHead className="w-[96px] text-right text-xs">{t('dashboard.count')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -368,16 +370,18 @@ export default function MailArchiveDashboard() {
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-primary opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="text-xs flex flex-wrap gap-x-1 min-w-0 flex-1">
<span className="flex items-center">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleQuickSearch({ from: s.key })
}}
className="hover:text-primary hover:underline transition-colors truncate max-w-[258px]"
>
{s.key}
</button>
<LongText className="max-w-[180px] md:max-w-[160px] lg:max-w-[200px] xl:max-w-[220px]">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleQuickSearch({ from: s.key })
}}
className="hover:text-primary hover:underline transition-colors"
>
{s.key}
</button>
</LongText>
</span>
</div>
</div>
@@ -392,17 +396,17 @@ export default function MailArchiveDashboard() {
)}
</CardContent>
</Card>
<Card>
<Card className="overflow-hidden">
<CardHeader className="!px-4 !pt-4 !pb-1">
<CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.top10LargestEmails')}</CardTitle>
</CardHeader>
<CardContent className="p-0">
{hasTopEmails ? (
<Table>
<Table className="table-fixed">
<TableHeader>
<TableRow>
<TableHead className="text-xs">{t('dashboard.subject')}</TableHead>
<TableHead className="text-right text-xs">{t('dashboard.size')}</TableHead>
<TableHead className="w-[96px] text-right text-xs">{t('dashboard.size')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -413,16 +417,18 @@ export default function MailArchiveDashboard() {
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-primary opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="text-xs flex flex-wrap gap-x-1 min-w-0 flex-1">
<span className="flex items-center">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleQuickSearch({ id: m.id })
}}
className="hover:text-primary hover:underline transition-colors truncate max-w-[258px]"
>
{m.subject || t('dashboard.noSubject')}
</button>
<LongText className="max-w-[180px] md:max-w-[160px] lg:max-w-[200px] xl:max-w-[220px]">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleQuickSearch({ id: m.id })
}}
className="hover:text-primary hover:underline transition-colors"
>
{m.subject || t('dashboard.noSubject')}
</button>
</LongText>
</span>
</div>
</div>
@@ -437,7 +443,7 @@ export default function MailArchiveDashboard() {
)}
</CardContent>
</Card>
<Card>
<Card className="overflow-hidden">
<CardHeader className="!px-4 !pt-4 !pb-1">
<CardTitle className="text-xs font-bold uppercase tracking-wider">
{t('dashboard.top10LargestAttachments')}
@@ -445,11 +451,11 @@ export default function MailArchiveDashboard() {
</CardHeader>
<CardContent className="p-0">
{stats?.top_largest_attachments?.length ? (
<Table>
<Table className="table-fixed">
<TableHeader>
<TableRow>
<TableHead className="text-xs">{t('attachment.name')}</TableHead>
<TableHead className="text-right text-xs">{t('dashboard.size')}</TableHead>
<TableHead className="w-[96px] text-right text-xs">{t('dashboard.size')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -463,16 +469,18 @@ export default function MailArchiveDashboard() {
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-primary opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="text-xs flex flex-wrap gap-x-1 min-w-0 flex-1">
<span className="flex items-center">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleQuickAttachmentSearch({ id: a.id })
}}
className="hover:text-primary hover:underline transition-colors truncate max-w-[238px]"
>
{a.name || 'Unnamed'}
</button>
<LongText className="max-w-[160px] md:max-w-[140px] lg:max-w-[180px] xl:max-w-[200px]">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleQuickAttachmentSearch({ id: a.id })
}}
className="hover:text-primary hover:underline transition-colors"
>
{a.name || 'Unnamed'}
</button>
</LongText>
</span>
</div>
</div>
@@ -489,17 +497,17 @@ export default function MailArchiveDashboard() {
)}
</CardContent>
</Card>
<Card>
<Card className="overflow-hidden">
<CardHeader className="!px-4 !pt-4 !pb-1">
<CardTitle className="text-xs font-bold uppercase tracking-wider">{t('dashboard.top10Accounts')}</CardTitle>
</CardHeader>
<CardContent className="p-0">
{hasTopAccounts ? (
<Table>
<Table className="table-fixed">
<TableHeader>
<TableRow>
<TableHead className="text-xs">{t('dashboard.account')}</TableHead>
<TableHead className="text-right text-xs">{t('dashboard.emails')}</TableHead>
<TableHead className="w-[96px] text-right text-xs">{t('dashboard.emails')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -510,16 +518,18 @@ export default function MailArchiveDashboard() {
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-primary opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="text-xs flex flex-wrap gap-x-1 min-w-0 flex-1">
<span className="flex items-center">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleQuickSearch({ account_ids: [getAccountIdByEmail(acc.key) || 0] })
}}
className="hover:text-primary hover:underline transition-colors truncate max-w-[258px]"
>
{acc.key}
</button>
<LongText className="max-w-[180px] md:max-w-[160px] lg:max-w-[200px] xl:max-w-[220px]">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleQuickSearch({ account_ids: [getAccountIdByEmail(acc.key) || 0] })
}}
className="hover:text-primary hover:underline transition-colors"
>
{acc.key}
</button>
</LongText>
</span>
</div>
</div>

View File

@@ -29,7 +29,8 @@ import {
Activity,
InfoIcon,
Mail,
Zap
Zap,
Bot
} from "lucide-react"
import { get_system_configurations } from "@/api/system/api"
import { useQuery } from "@tanstack/react-query"
@@ -180,6 +181,17 @@ export default function ServerConfigurationsPage() {
<SettingRow label="BICHON_SMTP_AUTH_REQUIRED" value={<BooleanBadge value={data!.bichon_smtp_auth_required} />} />
</SettingsCard>
<SettingsCard
icon={Bot}
title={t("systemConfig.sections.integration.title")}
description={t("systemConfig.sections.integration.desc")}
>
<SettingRow
label="BICHON_ENABLE_MCP"
value={<BooleanBadge value={data!.bichon_enable_mcp} />}
/>
</SettingsCard>
<SettingsCard
icon={Zap}
title={t("systemConfig.sections.performance.title")}

View File

@@ -1317,6 +1317,10 @@
"desc": "قواعد الوصول عبر الأصول",
"title": "CORS"
},
"integration": {
"desc": "تكامل بروتوكول المساعد الذكي.",
"title": "التكامل الخارجي"
},
"logging": {
"desc": "سلوك إخراج السجلات",
"title": "التسجيل"

View File

@@ -1317,6 +1317,10 @@
"desc": "Adgangsregler for cross-origin",
"title": "CORS"
},
"integration": {
"desc": "Integration af AI-assistentprotokol.",
"title": "Ekstern integration"
},
"logging": {
"desc": "Log-output adfærd",
"title": "Logning"

View File

@@ -1317,6 +1317,10 @@
"desc": "Cross-Origin-Zugriffsregeln",
"title": "CORS"
},
"integration": {
"desc": "Integration des KI-Assistenten-Protokolls.",
"title": "Externe Integration"
},
"logging": {
"desc": "Verhalten der Log-Ausgabe",
"title": "Protokollierung"

View File

@@ -1318,6 +1318,10 @@
"desc": "Cross-origin access rules",
"title": "CORS"
},
"integration": {
"desc": "AI assistant protocol integration.",
"title": "External Integration"
},
"logging": {
"desc": "Log output behavior",
"title": "Logging"

View File

@@ -1317,6 +1317,10 @@
"desc": "Reglas de acceso de origen cruzado",
"title": "CORS"
},
"integration": {
"desc": "Integración del protocolo del asistente de IA.",
"title": "Integración externa"
},
"logging": {
"desc": "Comportamiento de salida de logs",
"title": "Registro"

View File

@@ -1317,6 +1317,10 @@
"desc": "Cross-origin-käyttösäännöt",
"title": "CORS"
},
"integration": {
"desc": "Tekoälyavustajan protokollaintegraatio.",
"title": "Ulkoinen integraatio"
},
"logging": {
"desc": "Lokien tulostuskäyttäytyminen",
"title": "Lokitukset"

View File

@@ -1317,6 +1317,10 @@
"desc": "Règles d'accès cross-origin",
"title": "CORS"
},
"integration": {
"desc": "Intégration du protocole d'assistant IA.",
"title": "Intégration externe"
},
"logging": {
"desc": "Comportement de sortie des logs",
"title": "Journalisation"

View File

@@ -1317,6 +1317,10 @@
"desc": "Regole di accesso cross-origin",
"title": "CORS"
},
"integration": {
"desc": "Integrazione del protocollo dell'assistente IA.",
"title": "Integrazione esterna"
},
"logging": {
"desc": "Comportamento dell'output dei log",
"title": "Logging"

View File

@@ -1317,6 +1317,10 @@
"desc": "クロスオリジンアクセスのルール",
"title": "CORS"
},
"integration": {
"desc": "AIアシスタントプロトコルの統合。",
"title": "外部統合"
},
"logging": {
"desc": "ログ出力の動作",
"title": "ロギング"

View File

@@ -1317,6 +1317,10 @@
"desc": "교차 출처 액세스 규칙",
"title": "CORS"
},
"integration": {
"desc": "AI 어시스턴트 프로토콜 연동.",
"title": "외부 연동"
},
"logging": {
"desc": "로그 출력 동작",
"title": "로깅"

View File

@@ -1317,6 +1317,10 @@
"desc": "Cross-origin toegangsregels",
"title": "CORS"
},
"integration": {
"desc": "Integratie van AI-assistentprotocol.",
"title": "Externe integratie"
},
"logging": {
"desc": "Gedrag van log-output",
"title": "Logging"

View File

@@ -1317,6 +1317,10 @@
"desc": "Tilgangsregler for cross-origin",
"title": "CORS"
},
"integration": {
"desc": "Integrasjon av AI-assistentprotokoll.",
"title": "Ekstern integrasjon"
},
"logging": {
"desc": "Loggutdata-oppførsel",
"title": "Logging"

View File

@@ -1317,6 +1317,10 @@
"desc": "Zasady dostępu cross-origin",
"title": "CORS"
},
"integration": {
"desc": "Integracja protokołu asystenta AI.",
"title": "Zewnętrzna integracja"
},
"logging": {
"desc": "Zachowanie wyjściowe logów",
"title": "Logowanie"

View File

@@ -1317,6 +1317,10 @@
"desc": "Regras de acesso cross-origin",
"title": "CORS"
},
"integration": {
"desc": "Integração do protocolo do assistente de IA.",
"title": "Integração externa"
},
"logging": {
"desc": "Comportamento de saída de logs",
"title": "Logging"

View File

@@ -1317,6 +1317,10 @@
"desc": "Правила кросс-доменного доступа",
"title": "CORS"
},
"integration": {
"desc": "Интеграция протокола ИИ-ассистента.",
"title": "Внешняя интеграция"
},
"logging": {
"desc": "Поведение вывода логов",
"title": "Логирование"

View File

@@ -1317,6 +1317,10 @@
"desc": "Åtkomstregler för cross-origin",
"title": "CORS"
},
"integration": {
"desc": "Integrering av AI-assistentprotokoll.",
"title": "Extern integration"
},
"logging": {
"desc": "Loggutmatning",
"title": "Loggning"

View File

@@ -1317,6 +1317,10 @@
"desc": "跨來源資源共用規則",
"title": "CORS"
},
"integration": {
"desc": "AI 助手協定集成。",
"title": "外部集成"
},
"logging": {
"desc": "日誌輸出行為",
"title": "日誌"

View File

@@ -1318,6 +1318,10 @@
"desc": "跨域访问规则",
"title": "CORS"
},
"integration": {
"desc": "AI 助手协议集成。",
"title": "外部集成"
},
"logging": {
"desc": "日志输出行为",
"title": "日志"