From 66b595908cd38a0921ab580e2efd7ae73656a29d Mon Sep 17 00:00:00 2001 From: rustmailer Date: Fri, 8 May 2026 16:28:11 +0800 Subject: [PATCH] feat: detect legacy tantivy data layout and abort startup with migration hint --- crates/core/src/dashboard/mod.rs | 3 +- crates/core/src/lib.rs | 1 + crates/core/src/migrate/mod.rs | 62 +++++++++++++++++++++ crates/core/src/store/tantivy/attachment.rs | 26 ++++++++- crates/server/src/main.rs | 44 ++++++++++----- web/src/api/system/api.ts | 2 + web/src/features/accounts/index.tsx | 3 +- web/src/features/dashboard/index.tsx | 41 ++++++++++---- web/src/locales/ar.json | 2 + web/src/locales/da.json | 2 + web/src/locales/de.json | 2 + web/src/locales/en.json | 2 + web/src/locales/es.json | 2 + web/src/locales/fi.json | 2 + web/src/locales/fr.json | 2 + web/src/locales/it.json | 2 + web/src/locales/jp.json | 2 + web/src/locales/ko.json | 2 + web/src/locales/nl.json | 2 + web/src/locales/no.json | 2 + web/src/locales/pl.json | 2 + web/src/locales/pt.json | 2 + web/src/locales/ru.json | 2 + web/src/locales/sv.json | 2 + web/src/locales/zh-tw.json | 2 + web/src/locales/zh.json | 2 + 26 files changed, 189 insertions(+), 29 deletions(-) create mode 100644 crates/core/src/migrate/mod.rs diff --git a/crates/core/src/dashboard/mod.rs b/crates/core/src/dashboard/mod.rs index bf92970..f812c85 100644 --- a/crates/core/src/dashboard/mod.rs +++ b/crates/core/src/dashboard/mod.rs @@ -46,6 +46,7 @@ use crate::{ pub struct DashboardStats { pub account_count: usize, // Number of accounts pub email_count: u64, // Total number of emails + pub attachment_count: u64, // Total number of attachments pub total_size_bytes: u64, // Total size of all emails (in bytes) pub storage_usage_bytes: u64, // Actual storage used (in bytes) pub index_usage_bytes: u64, // Index storage size (in bytes) @@ -89,7 +90,7 @@ impl DashboardStats { }; stat.email_count = ENVELOPE_MANAGER.total_emails(&authorized_ids)?; - + stat.attachment_count = ATTACHMENT_MANAGER.total_attachments(&authorized_ids)?; if has_all_accounts { stat.storage_usage_bytes = get_total_size(&DATA_DIR_MANAGER.storage_dir) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index b9b8033..cdadcd6 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -13,6 +13,7 @@ pub mod import; pub mod logger; pub mod mailbox; pub mod message; +pub mod migrate; pub mod oauth2; pub mod settings; pub mod store; diff --git a/crates/core/src/migrate/mod.rs b/crates/core/src/migrate/mod.rs new file mode 100644 index 0000000..7d2feae --- /dev/null +++ b/crates/core/src/migrate/mod.rs @@ -0,0 +1,62 @@ +use std::path::PathBuf; + +use crate::settings::cli::SETTINGS; + +pub fn is_tantivy_index_dir(dir: &PathBuf) -> std::io::Result { + if !dir.exists() || !dir.is_dir() { + return Ok(false); + } + + let tantivy_extensions = [".store", ".term", ".idx", ".fieldnorm", ".pos"]; + let mut match_count = 0; + let mut has_meta_json = false; + + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let file_name = entry.file_name(); + let name = file_name.to_string_lossy(); + + if name == "meta.json" { + has_meta_json = true; + continue; + } + + if tantivy_extensions.iter().any(|ext| name.ends_with(ext)) { + match_count += 1; + } + } + + Ok(has_meta_json && match_count >= 3) +} + +pub fn is_legacy_data_layout() -> std::io::Result { + let root_dir = PathBuf::from(&SETTINGS.bichon_root_dir); + let envelope_dir = if let Some(ref index_dir) = SETTINGS.bichon_index_dir { + PathBuf::from(index_dir) + } else { + root_dir.join("envelope") + }; + + let eml_dir = if let Some(ref data_dir) = SETTINGS.bichon_data_dir { + PathBuf::from(data_dir) + } else { + root_dir.join("eml") + }; + + let envelope_result = is_tantivy_index_dir(&envelope_dir)?; + let eml_result = is_tantivy_index_dir(&eml_dir)?; + Ok(envelope_result || eml_result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_real_tantivy_dir() { + let path = PathBuf::from(r"D:\test-data\envelope"); + let result = is_tantivy_index_dir(&path).unwrap(); + println!("is tantivy index dir: {}", result); + assert!(result); + } +} diff --git a/crates/core/src/store/tantivy/attachment.rs b/crates/core/src/store/tantivy/attachment.rs index c2a23ef..29645ff 100644 --- a/crates/core/src/store/tantivy/attachment.rs +++ b/crates/core/src/store/tantivy/attachment.rs @@ -266,6 +266,30 @@ impl IndexManager { Box::new(boolean_query) } + pub fn total_attachments(&self, accounts: &Option>) -> BichonResult { + let searcher = self.create_searcher()?; + + match accounts { + Some(ref ids) if !ids.is_empty() => { + let mut subqueries = Vec::new(); + for &id in ids { + let term = Term::from_field_u64(SchemaTools::email_fields().f_account_id, id); + subqueries.push(( + Occur::Should, + Box::new(TermQuery::new(term, IndexRecordOption::Basic)) as Box, + )); + } + let query = Box::new(BooleanQuery::new(subqueries)) as Box; + let count = searcher + .search(&query, &Count) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(count as u64) + } + Some(_) => Ok(0), + None => Ok(searcher.num_docs()), + } + } + fn filter_query( &self, accounts: Option>, @@ -358,7 +382,7 @@ impl IndexManager { if let Some(ref name) = filter.attachment_name { let query_parser = QueryParser::for_index(&self.index, vec![f.f_name_text, f.f_name_exact]); - + let q = query_parser .parse_query(name) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InvalidParameter))?; diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index ab5cd72..1512153 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -19,20 +19,20 @@ use std::sync::LazyLock; use bichon_core::{ - bichon_version, raise_error, - { - cache::imap::task::SYNC_TASKS, - common::rustls::BichonTls, - context::{executors::BichonContext, Initialize}, - error::{code::ErrorCode, BichonResult}, - logger, - settings::cli::SETTINGS, - store::{ - storage::BLOB_MANAGER, - tantivy::{attachment::ATTACHMENT_MANAGER, envelope::ENVELOPE_MANAGER}, - }, - tasks::PeriodicTasks, + bichon_version, + cache::imap::task::SYNC_TASKS, + common::rustls::BichonTls, + context::{executors::BichonContext, Initialize}, + error::{code::ErrorCode, BichonResult}, + logger, + migrate::is_legacy_data_layout, + raise_error, + settings::cli::SETTINGS, + store::{ + storage::BLOB_MANAGER, + tantivy::{attachment::ATTACHMENT_MANAGER, envelope::ENVELOPE_MANAGER}, }, + tasks::PeriodicTasks, }; use bichon_smtp::server::{start_smtp_server, SmtpServer}; use mimalloc::MiMalloc; @@ -69,6 +69,24 @@ async fn main() -> BichonResult<()> { info!("Git: [{}]", env!("GIT_HASH")); info!("GitHub: https://github.com/rustmailer/bichon"); + match is_legacy_data_layout() { + Ok(true) => { + error!("Incompatible data format detected."); + error!("Your data was created by an older version of Bichon and must be migrated before use."); + error!("Please run: bichon-migrate"); + error!("Documentation: https://github.com/rustmailer/bichon/wiki/migration"); + return Err(raise_error!( + "Legacy data layout detected".into(), + ErrorCode::InternalError + )); + } + Err(e) => { + error!("Failed to check data layout: {:#?}", e); + return Err(raise_error!(format!("{:#?}", e), ErrorCode::InternalError)); + } + Ok(false) => {} + } + if let Err(error) = initialize().await { eprintln!("{:?}", error); return Err(error); diff --git a/web/src/api/system/api.ts b/web/src/api/system/api.ts index 6cae7c6..21645dc 100644 --- a/web/src/api/system/api.ts +++ b/web/src/api/system/api.ts @@ -44,6 +44,7 @@ export const get_notifications = async () => { export interface DashboardStats { account_count: number; // Number of accounts email_count: number; // Total number of emails + attachment_count: number; // Total number of attachments total_size_bytes: number; // Total size of all emails (in bytes) storage_usage_bytes: number; // Actual storage used (in bytes) index_usage_bytes: number; // Index storage size (in bytes) @@ -60,6 +61,7 @@ export interface DashboardStats { export const INITIAL_DASHBOARD_STATS: DashboardStats = { account_count: 0, email_count: 0, + attachment_count: 0, total_size_bytes: 0, storage_usage_bytes: 0, index_usage_bytes: 0, diff --git a/web/src/features/accounts/index.tsx b/web/src/features/accounts/index.tsx index 9c30f01..dc1c5d4 100644 --- a/web/src/features/accounts/index.tsx +++ b/web/src/features/accounts/index.tsx @@ -28,7 +28,7 @@ import { AccountTable } from './components/table' import AccountProvider, { type AccountDialogType, } from './context' -import { MoreVertical, Plus } from 'lucide-react' +import { Plus } from 'lucide-react' import Logo from '@/assets/logo.svg' import { AccountDetailDrawer } from './components/account-detail' import { AccountModel, list_accounts } from '@/api/account/api' @@ -39,7 +39,6 @@ import { RunningStateDialog } from './components/running-state-dialog' import { FixedHeader } from '@/components/layout/fixed-header' import { DownloadFoldersDialog } from './components/download-folders' import { NoSyncAccountDialog } from './components/nosync-dialog' -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { useTranslation } from 'react-i18next' import { AccountAccessAssignmentDialog } from './components/access-assignment-dialog' import { useCurrentUser } from '@/hooks/use-current-user' diff --git a/web/src/features/dashboard/index.tsx b/web/src/features/dashboard/index.tsx index ba32b64..adbb604 100644 --- a/web/src/features/dashboard/index.tsx +++ b/web/src/features/dashboard/index.tsx @@ -219,21 +219,15 @@ export default function MailArchiveDashboard() {

{t('dashboard.syncedLocally')}

+ - {t('dashboard.systemVersion')} + {t('dashboard.totalAttachments')} + -
- {stats1.system_version ? ( - - {stats1.system_version} - - ) : 'N/A'} -
-
- -
+
{formatNumber(stats1.attachment_count)}
+

{t('dashboard.regularAttachments')}

@@ -558,7 +552,30 @@ export default function MailArchiveDashboard() {
- © 2025-2026 rustmailer.com - Bichon Email Archiving Project +

+ © 2025-2026{" "} + + Bichon Email Archiving Project + + {stats1.system_version && ( + <> + + + v{stats1.system_version} + + + )} +

); diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json index 7e5ad65..e12d725 100644 --- a/web/src/locales/ar.json +++ b/web/src/locales/ar.json @@ -484,6 +484,7 @@ "noRecentActivity": "لا يوجد نشاط حديث", "noSendersData": "لا توجد بيانات للمرسلين", "noSubject": "(لا يوجد موضوع)", + "regularAttachments": "المرفقات العادية", "saved": "الموفرة", "sender": "المرسل", "size": "الحجم", @@ -497,6 +498,7 @@ "top10LargestEmails": "أكبر 10 رسائل بريد إلكتروني", "top10Senders": "أفضل 10 مرسلين", "topLists": "القوائم الأعلى", + "totalAttachments": "إجمالي المرفقات", "totalEmailSize": "الحجم الإجمالي لرسائل البريد الإلكتروني", "totalEmails": "إجمالي رسائل البريد الإلكتروني", "withAttachments": "مع مرفقات" diff --git a/web/src/locales/da.json b/web/src/locales/da.json index d8c30e1..be1d701 100644 --- a/web/src/locales/da.json +++ b/web/src/locales/da.json @@ -484,6 +484,7 @@ "noRecentActivity": "Ingen nylig aktivitet", "noSendersData": "Ingen afsenderdata", "noSubject": "(Intet emne)", + "regularAttachments": "Almindelige vedhæftede filer", "saved": "Sparet", "sender": "Afender", "size": "Størrelse", @@ -497,6 +498,7 @@ "top10LargestEmails": "Top 10 Største E-mails", "top10Senders": "Top 10 Afsendere", "topLists": "Toplister", + "totalAttachments": "Samlet antal vedhæftede filer", "totalEmailSize": "Samlet e-mailstørrelse", "totalEmails": "Samlet antal e-mails", "withAttachments": "Med vedhæftninger" diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 6b32fb9..e0b9190 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -484,6 +484,7 @@ "noRecentActivity": "Keine kürzliche Aktivität", "noSendersData": "Keine Absenderdaten", "noSubject": "(Kein Betreff)", + "regularAttachments": "Reguläre Anhänge", "saved": "Eingespart", "sender": "Absender", "size": "Größe", @@ -497,6 +498,7 @@ "top10LargestEmails": "Top 10 größte E-Mails", "top10Senders": "Top 10 Absender", "topLists": "Top-Listen", + "totalAttachments": "Anhänge insgesamt", "totalEmailSize": "Gesamtgröße der E-Mails", "totalEmails": "Gesamt-E-Mails", "withAttachments": "Mit Anhängen" diff --git a/web/src/locales/en.json b/web/src/locales/en.json index ae2d206..d3019f1 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -484,6 +484,7 @@ "noRecentActivity": "No recent activity", "noSendersData": "No senders data", "noSubject": "(No Subject)", + "regularAttachments": "Regular attachments", "saved": "Saved", "sender": "Sender", "size": "Size", @@ -497,6 +498,7 @@ "top10LargestEmails": "Top 10 Largest Emails", "top10Senders": "Top 10 Senders", "topLists": "Top Lists", + "totalAttachments": "Total attachments", "totalEmailSize": "Total Email Size", "totalEmails": "Total Emails", "withAttachments": "With Attachments" diff --git a/web/src/locales/es.json b/web/src/locales/es.json index 1145596..2688c40 100644 --- a/web/src/locales/es.json +++ b/web/src/locales/es.json @@ -484,6 +484,7 @@ "noRecentActivity": "Sin actividad reciente", "noSendersData": "Sin datos de remitentes", "noSubject": "(Sin asunto)", + "regularAttachments": "Adjuntos regulares", "saved": "Ahorrado", "sender": "Remitente", "size": "Tamaño", @@ -497,6 +498,7 @@ "top10LargestEmails": "Los 10 correos más grandes", "top10Senders": "Los 10 principales remitentes", "topLists": "Listas principales", + "totalAttachments": "Total de adjuntos", "totalEmailSize": "Tamaño total de correos", "totalEmails": "Total de correos", "withAttachments": "Con adjuntos" diff --git a/web/src/locales/fi.json b/web/src/locales/fi.json index 948bbfc..14bd36d 100644 --- a/web/src/locales/fi.json +++ b/web/src/locales/fi.json @@ -484,6 +484,7 @@ "noRecentActivity": "Ei viimeaikaisia tapahtumia", "noSendersData": "Ei lähettäjätietoja", "noSubject": "(Ei aihetta)", + "regularAttachments": "Tavalliset liitteet", "saved": "Säästetty", "sender": "Lähettäjä", "size": "Koko", @@ -497,6 +498,7 @@ "top10LargestEmails": "10 suurinta sähköpostia", "top10Senders": "10 parasta lähettäjää", "topLists": "Parhaat listat", + "totalAttachments": "Liitteitä yhteensä", "totalEmailSize": "Sähköpostien kokonaiskoko", "totalEmails": "Sähköpostien kokonaismäärä", "withAttachments": "Liitteillä" diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json index b9aea65..32e9a47 100644 --- a/web/src/locales/fr.json +++ b/web/src/locales/fr.json @@ -484,6 +484,7 @@ "noRecentActivity": "Aucune activité récente", "noSendersData": "Aucune donnée d'expéditeurs", "noSubject": "(Aucun Objet)", + "regularAttachments": "Pièces jointes standard", "saved": "Économisé", "sender": "Expéditeur", "size": "Taille", @@ -497,6 +498,7 @@ "top10LargestEmails": "Top 10 des E-mails les Plus Volumineux", "top10Senders": "Top 10 des Expéditeurs", "topLists": "Top Listes", + "totalAttachments": "Total des pièces jointes", "totalEmailSize": "Taille totale des e-mails", "totalEmails": "Total des e-mails", "withAttachments": "Avec Pièces Jointes" diff --git a/web/src/locales/it.json b/web/src/locales/it.json index 5f3388f..450c959 100644 --- a/web/src/locales/it.json +++ b/web/src/locales/it.json @@ -484,6 +484,7 @@ "noRecentActivity": "Nessuna attività recente", "noSendersData": "Nessun dato sui mittenti", "noSubject": "(Nessun Oggetto)", + "regularAttachments": "Allegati regolari", "saved": "Risparmiato", "sender": "Mittente", "size": "Dimensione", @@ -497,6 +498,7 @@ "top10LargestEmails": "Le 10 Email più Grandi", "top10Senders": "I 10 Mittenti Principali", "topLists": "Liste Top", + "totalAttachments": "Allegati totali", "totalEmailSize": "Dimensione totale email", "totalEmails": "Email totali", "withAttachments": "Con Allegati" diff --git a/web/src/locales/jp.json b/web/src/locales/jp.json index 92a933c..9bfb07d 100644 --- a/web/src/locales/jp.json +++ b/web/src/locales/jp.json @@ -484,6 +484,7 @@ "noRecentActivity": "最近のアクティビティなし", "noSendersData": "送信者データなし", "noSubject": "(件名なし)", + "regularAttachments": "通常の添付ファイル", "saved": "節約済み", "sender": "送信者", "size": "サイズ", @@ -497,6 +498,7 @@ "top10LargestEmails": "容量の大きいメールトップ10", "top10Senders": "送信者トップ10", "topLists": "トップリスト", + "totalAttachments": "添付ファイル総数", "totalEmailSize": "メール総容量", "totalEmails": "合計メール数", "withAttachments": "添付ファイルあり" diff --git a/web/src/locales/ko.json b/web/src/locales/ko.json index 313d5c4..23ce3ee 100644 --- a/web/src/locales/ko.json +++ b/web/src/locales/ko.json @@ -484,6 +484,7 @@ "noRecentActivity": "최근 활동 없음", "noSendersData": "발신자 데이터 없음", "noSubject": "(제목 없음)", + "regularAttachments": "일반 첨부 파일", "saved": "절약됨", "sender": "발신자", "size": "크기", @@ -497,6 +498,7 @@ "top10LargestEmails": "상위 10개 최대 크기 이메일", "top10Senders": "상위 10명 발신자", "topLists": "상위 목록", + "totalAttachments": "총 첨부 파일", "totalEmailSize": "총 이메일 크기", "totalEmails": "총 이메일 수", "withAttachments": "첨부 파일 포함" diff --git a/web/src/locales/nl.json b/web/src/locales/nl.json index 2d750c1..66140cc 100644 --- a/web/src/locales/nl.json +++ b/web/src/locales/nl.json @@ -484,6 +484,7 @@ "noRecentActivity": "Geen recente activiteit", "noSendersData": "Geen afzendersgegevens", "noSubject": "(Geen Onderwerp)", + "regularAttachments": "Reguliere bijlagen", "saved": "Bespaard", "sender": "Afzender", "size": "Grootte", @@ -497,6 +498,7 @@ "top10LargestEmails": "Top 10 Grootste e-mails", "top10Senders": "Top 10 Afzenders", "topLists": "Toplijsten", + "totalAttachments": "Totaal aantal bijlagen", "totalEmailSize": "Totale e-mailgrootte", "totalEmails": "Totaal aantal e-mails", "withAttachments": "Met Bijlagen" diff --git a/web/src/locales/no.json b/web/src/locales/no.json index e6715bb..f22a1ee 100644 --- a/web/src/locales/no.json +++ b/web/src/locales/no.json @@ -484,6 +484,7 @@ "noRecentActivity": "Ingen nylig aktivitet", "noSendersData": "Ingen avsenderdata", "noSubject": "(Uten emne)", + "regularAttachments": "Vanlige vedlegg", "saved": "Spart", "sender": "Avsender", "size": "Størrelse", @@ -497,6 +498,7 @@ "top10LargestEmails": "Topp 10 største e-poster", "top10Senders": "Topp 10 avsendere", "topLists": "Topplister", + "totalAttachments": "Totalt antall vedlegg", "totalEmailSize": "Total e-poststørrelse", "totalEmails": "Totalt antall e-poster", "withAttachments": "Med vedlegg" diff --git a/web/src/locales/pl.json b/web/src/locales/pl.json index eaeb183..afe7bbb 100644 --- a/web/src/locales/pl.json +++ b/web/src/locales/pl.json @@ -484,6 +484,7 @@ "noRecentActivity": "Brak aktywności", "noSendersData": "Brak danych od Nadawcy", "noSubject": "(brak tematu)", + "regularAttachments": "Zwykłe załączniki", "saved": "Zaoszczędzone", "sender": "Nadawca", "size": "Rozmiar", @@ -497,6 +498,7 @@ "top10LargestEmails": "Top 10 największych wiadomości", "top10Senders": "Top 10 Nadawców", "topLists": "Lista TOP", + "totalAttachments": "Łączna liczba załączników", "totalEmailSize": "Wielkość wszystkich wiadomości", "totalEmails": "Wszystkie wiadomości", "withAttachments": "Z załącznikami" diff --git a/web/src/locales/pt.json b/web/src/locales/pt.json index 88bc5b5..aeed73e 100644 --- a/web/src/locales/pt.json +++ b/web/src/locales/pt.json @@ -484,6 +484,7 @@ "noRecentActivity": "Nenhuma Atividade Recente", "noSendersData": "Sem Dados de Remetentes", "noSubject": "(Sem Assunto)", + "regularAttachments": "Anexos regulares", "saved": "Economizado", "sender": "Remetente", "size": "Tamanho", @@ -497,6 +498,7 @@ "top10LargestEmails": "Top 10 Maiores Emails", "top10Senders": "Top 10 Remetentes", "topLists": "Principais Listas", + "totalAttachments": "Total de anexos", "totalEmailSize": "Tamanho Total do Email", "totalEmails": "Total de Emails", "withAttachments": "Com Anexos" diff --git a/web/src/locales/ru.json b/web/src/locales/ru.json index af12855..4051946 100644 --- a/web/src/locales/ru.json +++ b/web/src/locales/ru.json @@ -484,6 +484,7 @@ "noRecentActivity": "Нет недавней активности", "noSendersData": "Нет данных об отправителях", "noSubject": "(Без темы)", + "regularAttachments": "Обычные вложения", "saved": "Сэкономлено", "sender": "Отправитель", "size": "Размер", @@ -497,6 +498,7 @@ "top10LargestEmails": "Топ 10 самых больших писем", "top10Senders": "Топ 10 отправителей", "topLists": "Топ списки", + "totalAttachments": "Всего вложений", "totalEmailSize": "Общий размер писем", "totalEmails": "Всего писем", "withAttachments": "С вложениями" diff --git a/web/src/locales/sv.json b/web/src/locales/sv.json index 0bd1182..72c9255 100644 --- a/web/src/locales/sv.json +++ b/web/src/locales/sv.json @@ -484,6 +484,7 @@ "noRecentActivity": "Ingen nylig aktivitet", "noSendersData": "Inga avsändardata", "noSubject": "(Inget ämne)", + "regularAttachments": "Vanliga bilagor", "saved": "Sparat", "sender": "Avsändare", "size": "Storlek", @@ -497,6 +498,7 @@ "top10LargestEmails": "Topp 10 största e-postmeddelanden", "top10Senders": "Topp 10 avsändare", "topLists": "Topplistor", + "totalAttachments": "Totalt antal bilagor", "totalEmailSize": "Total e-poststorlek", "totalEmails": "Totalt antal e-postmeddelanden", "withAttachments": "Med bilagor" diff --git a/web/src/locales/zh-tw.json b/web/src/locales/zh-tw.json index da0b76d..977cf92 100644 --- a/web/src/locales/zh-tw.json +++ b/web/src/locales/zh-tw.json @@ -484,6 +484,7 @@ "noRecentActivity": "無近期活動", "noSendersData": "無寄件人資料", "noSubject": "(無主旨)", + "regularAttachments": "普通附件", "saved": "節省空間", "sender": "寄件人", "size": "大小", @@ -497,6 +498,7 @@ "top10LargestEmails": "前 10 封最大郵件", "top10Senders": "前 10 名寄件人", "topLists": "排行榜", + "totalAttachments": "附件總數", "totalEmailSize": "郵件總容量", "totalEmails": "郵件總數", "withAttachments": "包含附件" diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index 468a39a..efd1a5b 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -484,6 +484,7 @@ "noRecentActivity": "无最近活动", "noSendersData": "无发件人数据", "noSubject": "(无主题)", + "regularAttachments": "普通附件", "saved": "节省空间", "sender": "发件人", "size": "大小", @@ -497,6 +498,7 @@ "top10LargestEmails": "前10大邮件", "top10Senders": "前10名发件人", "topLists": "排行榜", + "totalAttachments": "附件总数", "totalEmailSize": "邮件总大小", "totalEmails": "邮件总数", "withAttachments": "包含附件"