From 1f57f372d329a15c9c76b3dc90d497511f14f52b Mon Sep 17 00:00:00 2001 From: rustmailer Date: Sun, 28 Dec 2025 13:01:34 +0800 Subject: [PATCH] feat: Add sync_batch_size to allow users to customize the synchronization batch size, and introduce date_before to support semantics such as downloading emails from more than one year ago. #24 #58 --- src/modules/account/migration.rs | 23 +- src/modules/account/payload.rs | 34 +- src/modules/account/since.rs | 1 - src/modules/account/view.rs | 6 +- src/modules/cache/imap/sync/flow.rs | 17 +- src/modules/imap/executor.rs | 21 +- web/package.json | 2 +- web/pnpm-lock.yaml | 33 +- web/src/api/account/api.ts | 54 ++- .../components/access-assignment-dialog.tsx | 4 +- .../accounts/components/account-detail.tsx | 69 +++- .../accounts/components/action-dialog.tsx | 140 ++++---- .../features/accounts/components/columns.tsx | 5 +- .../components/data-table-row-actions.tsx | 4 +- .../accounts/components/delete-dialog.tsx | 3 +- .../accounts/components/enable-action.tsx | 3 +- .../accounts/components/nosync-dialog.tsx | 3 +- .../accounts/components/oauth2-action.tsx | 2 +- .../accounts/components/oauth2-tokens.tsx | 2 +- .../components/running-state-action.tsx | 2 +- .../components/running-state-dialog.tsx | 3 +- .../features/accounts/components/step3.tsx | 317 +++++++++--------- .../features/accounts/components/step4.tsx | 69 +++- .../accounts/components/sync-folders.tsx | 3 +- .../features/accounts/components/table.tsx | 2 +- web/src/features/accounts/context/index.tsx | 2 +- web/src/features/accounts/data/schema.ts | 67 ---- web/src/features/accounts/index.tsx | 3 +- web/src/locales/ar.json | 16 + web/src/locales/da.json | 16 + web/src/locales/de.json | 16 + web/src/locales/en.json | 16 + web/src/locales/es.json | 16 + web/src/locales/fi.json | 16 + web/src/locales/fr.json | 16 + web/src/locales/it.json | 16 + web/src/locales/jp.json | 16 + web/src/locales/ko.json | 16 + web/src/locales/nl.json | 16 + web/src/locales/no.json | 16 + web/src/locales/pl.json | 16 + web/src/locales/pt.json | 16 + web/src/locales/ru.json | 16 + web/src/locales/sv.json | 16 + web/src/locales/zh-tw.json | 16 + web/src/locales/zh.json | 16 + 46 files changed, 831 insertions(+), 351 deletions(-) delete mode 100644 web/src/features/accounts/data/schema.ts diff --git a/src/modules/account/migration.rs b/src/modules/account/migration.rs index cce3fa4..8d41934 100644 --- a/src/modules/account/migration.rs +++ b/src/modules/account/migration.rs @@ -27,7 +27,11 @@ use tracing::info; use crate::{ encrypt, modules::{ - account::{entity::ImapConfig, since::DateSince, state::AccountRunningState}, + account::{ + entity::ImapConfig, + since::{DateSince, RelativeDate}, + state::AccountRunningState, + }, cache::imap::mailbox::MailBox, database::{list_all_impl, with_transaction}, error::BichonResult, @@ -136,10 +140,12 @@ pub struct AccountV3 { pub name: Option, pub capabilities: Option>, pub date_since: Option, + pub date_before: Option, pub folder_limit: Option, pub sync_folders: Option>, pub account_type: AccountType, pub sync_interval_min: Option, + pub sync_batch_size: Option, pub known_folders: Option>, pub created_at: i64, pub updated_at: i64, @@ -174,6 +180,8 @@ impl AccountV3 { use_dangerous: request.use_dangerous, pgp_key: request.pgp_key, created_by: user_id, + sync_batch_size: request.sync_batch_size, + date_before: request.date_before, }) } @@ -404,6 +412,12 @@ impl AccountV3 { if let Some(date_since) = request.date_since { new.date_since = Some(date_since); + new.date_before = None; + } + + if let Some(date_before) = request.date_before { + new.date_before = Some(date_before); + new.date_since = None; } if let Some(folder_limit) = request.folder_limit { @@ -439,6 +453,11 @@ impl AccountV3 { if let Some(sync_interval_min) = &request.sync_interval_min { new.sync_interval_min = Some(*sync_interval_min); } + + if let Some(sync_batch_size) = &request.sync_batch_size { + new.sync_batch_size = Some(*sync_batch_size); + } + if let Some(use_proxy) = request.use_proxy { new.use_proxy = Some(use_proxy); } @@ -558,6 +577,8 @@ impl From for AccountV3 { use_proxy: value.use_proxy, use_dangerous: value.use_dangerous, pgp_key: value.pgp_key, + sync_batch_size: None, + date_before: None, } } } diff --git a/src/modules/account/payload.rs b/src/modules/account/payload.rs index 78a2358..5be9c83 100644 --- a/src/modules/account/payload.rs +++ b/src/modules/account/payload.rs @@ -18,7 +18,7 @@ use crate::modules::account::entity::ImapConfig; use crate::modules::account::migration::{AccountModel, AccountType}; -use crate::modules::account::since::DateSince; +use crate::modules::account::since::{DateSince, RelativeDate}; use crate::modules::error::code::ErrorCode; use crate::modules::error::BichonResult; use crate::{raise_error, validate_email}; @@ -33,11 +33,14 @@ pub struct AccountCreateRequest { pub imap: Option, pub enabled: bool, pub date_since: Option, + pub date_before: Option, pub account_type: AccountType, #[oai(validator(minimum(value = "100")))] pub folder_limit: Option, #[oai(validator(minimum(value = "10"), maximum(value = "480")))] pub sync_interval_min: Option, + #[oai(validator(minimum(value = "30"), maximum(value = "200")))] + pub sync_batch_size: Option, pub use_proxy: Option, pub use_dangerous: bool, pub pgp_key: Option, @@ -45,9 +48,22 @@ pub struct AccountCreateRequest { impl AccountCreateRequest { pub fn create_entity(self, user_id: u64) -> BichonResult { + if self.date_before.is_some() && self.date_since.is_some() { + return Err(raise_error!( + "date_before and date_since are mutually exclusive; specify only one time boundary" + .into(), + ErrorCode::InvalidParameter + )); + } + if let Some(date_since) = self.date_since.as_ref() { date_since.validate()?; } + + if let Some(date_before) = self.date_before.as_ref() { + date_before.validate_date()?; + } + match self.account_type { AccountType::IMAP => { match &self.imap { @@ -104,6 +120,7 @@ pub struct AccountUpdateRequest { /// - First-time sync optimization for large accounts /// - Reducing server load during resyncs pub date_since: Option, + pub date_before: Option, /// Max emails to sync for this folder. /// If not set, sync all emails. /// otherwise sync up to `n` most recent emails (min 10). @@ -126,6 +143,8 @@ pub struct AccountUpdateRequest { /// Incremental sync interval (seconds) #[oai(validator(minimum(value = "10"), maximum(value = "480")))] pub sync_interval_min: Option, + #[oai(validator(minimum(value = "30"), maximum(value = "200")))] + pub sync_batch_size: Option, /// Optional proxy ID for establishing the connection to external APIs (e.g., Gmail, Outlook). /// - If `None` or not provided, the client will connect directly to the API server. /// - If `Some(proxy_id)`, the client will use the pre-configured proxy with the given ID for API requests. @@ -138,9 +157,22 @@ pub struct AccountUpdateRequest { impl AccountUpdateRequest { pub fn validate_update_request(&self, account: &AccountModel) -> BichonResult<()> { + if self.date_before.is_some() && self.date_since.is_some() { + return Err(raise_error!( + "date_before and date_since are mutually exclusive; specify only one time boundary" + .into(), + ErrorCode::InvalidParameter + )); + } + if let Some(date_since) = self.date_since.as_ref() { date_since.validate()?; } + + if let Some(date_before) = self.date_before.as_ref() { + date_before.validate_date()?; + } + if matches!(account.account_type, AccountType::IMAP) { if let Some(mailboxes) = self.sync_folders.as_ref() { if mailboxes.is_empty() { diff --git a/src/modules/account/since.rs b/src/modules/account/since.rs index 5ae340f..1336769 100644 --- a/src/modules/account/since.rs +++ b/src/modules/account/since.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - use crate::{ modules::error::{code::ErrorCode, BichonResult}, raise_error, diff --git a/src/modules/account/view.rs b/src/modules/account/view.rs index 7c34ba1..447222b 100644 --- a/src/modules/account/view.rs +++ b/src/modules/account/view.rs @@ -25,7 +25,7 @@ use crate::modules::{ account::{ entity::ImapConfig, migration::{AccountModel, AccountType}, - since::DateSince, + since::{DateSince, RelativeDate}, }, users::BichonUser, }; @@ -39,10 +39,12 @@ pub struct AccountResp { pub name: Option, pub capabilities: Option>, pub date_since: Option, + pub date_before: Option, pub folder_limit: Option, pub sync_folders: Option>, pub account_type: AccountType, pub sync_interval_min: Option, + pub sync_batch_size: Option, pub known_folders: Option>, pub created_at: i64, pub updated_at: i64, @@ -65,10 +67,12 @@ impl AccountResp { name: account.name, capabilities: account.capabilities, date_since: account.date_since, + date_before: account.date_before, folder_limit: account.folder_limit, sync_folders: account.sync_folders, account_type: account.account_type, sync_interval_min: account.sync_interval_min, + sync_batch_size: account.sync_batch_size, known_folders: account.known_folders, created_at: account.created_at, updated_at: account.updated_at, diff --git a/src/modules/cache/imap/sync/flow.rs b/src/modules/cache/imap/sync/flow.rs index b52d6f7..07e230e 100644 --- a/src/modules/cache/imap/sync/flow.rs +++ b/src/modules/cache/imap/sync/flow.rs @@ -16,7 +16,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - use crate::{ modules::{ account::{migration::AccountModel, state::AccountRunningState}, @@ -37,7 +36,7 @@ use crate::{ use std::time::Instant; use tracing::{debug, error, info, warn}; -pub const BATCH_SIZE: u32 = 50; +pub const DEFAULT_BATCH_SIZE: u32 = 50; pub async fn fetch_and_save_since_date( account: &AccountModel, @@ -69,7 +68,11 @@ pub async fn fetch_and_save_since_date( // let semaphore = Arc::new(Semaphore::new(5)); - let uid_batches = generate_uid_sequence_hashset(uid_vec, BATCH_SIZE as usize, false); + let uid_batches = generate_uid_sequence_hashset( + uid_vec, + account.sync_batch_size.unwrap_or(DEFAULT_BATCH_SIZE) as usize, + false, + ); AccountRunningState::set_initial_current_syncing_folder( account_id, mailbox.name.clone(), @@ -105,9 +108,11 @@ pub async fn fetch_and_save_full_mailbox( _ => total, }; let page_size = if let Some(limit) = folder_limit { - limit.max(100).min(BATCH_SIZE as u32) + limit + .max(100) + .min(account.sync_batch_size.unwrap_or(DEFAULT_BATCH_SIZE)) } else { - BATCH_SIZE as u32 + account.sync_batch_size.unwrap_or(DEFAULT_BATCH_SIZE) }; let total_batches = total_to_fetch.div_ceil(page_size); @@ -349,7 +354,7 @@ async fn perform_incremental_sync( Some(max_uid) => { let executor = MAIL_CONTEXT.imap(account.id).await?; executor - .fetch_new_mail(account.id, local_mailbox, max_uid + 1) + .fetch_new_mail(account, local_mailbox, max_uid + 1) .await?; } None => { diff --git a/src/modules/imap/executor.rs b/src/modules/imap/executor.rs index 13f1320..85d02ef 100644 --- a/src/modules/imap/executor.rs +++ b/src/modules/imap/executor.rs @@ -16,9 +16,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use crate::modules::account::migration::AccountModel; use crate::modules::account::state::AccountRunningState; use crate::modules::cache::imap::mailbox::MailBox; -use crate::modules::cache::imap::sync::flow::{generate_uid_sequence_hashset, BATCH_SIZE}; +use crate::modules::cache::imap::sync::flow::{generate_uid_sequence_hashset, DEFAULT_BATCH_SIZE}; use crate::modules::envelope::extractor::extract_envelope; use crate::modules::error::code::ErrorCode; use crate::modules::indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER}; @@ -80,7 +81,7 @@ impl ImapExecutor { pub async fn fetch_new_mail( &self, - account_id: u64, + account: &AccountModel, mailbox: &MailBox, start_uid: u64, ) -> BichonResult<()> { @@ -98,17 +99,21 @@ impl ImapExecutor { } info!( "[account {}][mailbox {}] {} envelopes need to be fetched", - account_id, mailbox.name, len + account.id, mailbox.name, len ); let mut uid_vec: Vec = uid_list.into_iter().collect(); uid_vec.sort(); - let uid_batches = generate_uid_sequence_hashset(uid_vec, BATCH_SIZE as usize, false); + let uid_batches = generate_uid_sequence_hashset( + uid_vec, + account.sync_batch_size.unwrap_or(DEFAULT_BATCH_SIZE) as usize, + false, + ); - let too_many = len as u32 > 10 * BATCH_SIZE; + let too_many = len as u32 > 5 * account.sync_batch_size.unwrap_or(DEFAULT_BATCH_SIZE); if too_many { AccountRunningState::set_initial_current_syncing_folder( - account_id, + account.id, mailbox.name.clone(), uid_batches.len() as u32, ) @@ -118,13 +123,13 @@ impl ImapExecutor { for (index, batch) in uid_batches.into_iter().enumerate() { if too_many { AccountRunningState::set_current_sync_batch_number( - account_id, + account.id, mailbox.name.clone(), (index + 1) as u32, ) .await?; } - self.uid_batch_retrieve_emails(account_id, mailbox.id, &batch, &mailbox.encoded_name()) + self.uid_batch_retrieve_emails(account.id, mailbox.id, &batch, &mailbox.encoded_name()) .await?; } Ok(()) diff --git a/web/package.json b/web/package.json index 7725942..4b50984 100644 --- a/web/package.json +++ b/web/package.json @@ -61,7 +61,7 @@ "radix-ui": "^1.4.3", "react": "^18.3.1", "react-ace": "^13.0.0", - "react-day-picker": "8.10.1", + "react-day-picker": "9.13.0", "react-dom": "^18.3.1", "react-hook-form": "^7.54.0", "react-i18next": "^16.3.5", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 4845b1a..cf62009 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -153,8 +153,8 @@ importers: specifier: ^13.0.0 version: 13.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-day-picker: - specifier: 8.10.1 - version: 8.10.1(date-fns@3.6.0)(react@18.3.1) + specifier: 9.13.0 + version: 9.13.0(react@18.3.1) react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) @@ -402,6 +402,9 @@ packages: '@types/react': optional: true + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -3362,9 +3365,15 @@ packages: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -4326,11 +4335,11 @@ packages: react: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-day-picker@8.10.1: - resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + react-day-picker@9.13.0: + resolution: {integrity: sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==} + engines: {node: '>=18'} peerDependencies: - date-fns: ^2.28.0 || ^3.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: '>=16.8.0' react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} @@ -5166,6 +5175,8 @@ snapshots: optionalDependencies: '@types/react': 18.3.18 + '@date-fns/tz@1.4.1': {} + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.25.9 @@ -8083,8 +8094,12 @@ snapshots: d3-timer@3.0.1: {} + date-fns-jalali@4.1.0-0: {} + date-fns@3.6.0: {} + date-fns@4.1.0: {} + debug@4.3.7: dependencies: ms: 2.1.3 @@ -9262,9 +9277,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-day-picker@8.10.1(date-fns@3.6.0)(react@18.3.1): + react-day-picker@9.13.0(react@18.3.1): dependencies: - date-fns: 3.6.0 + '@date-fns/tz': 1.4.1 + date-fns: 4.1.0 + date-fns-jalali: 4.1.0-0 react: 18.3.1 react-dom@18.3.1(react@18.3.1): diff --git a/web/src/api/account/api.ts b/web/src/api/account/api.ts index 2134663..6468d02 100644 --- a/web/src/api/account/api.ts +++ b/web/src/api/account/api.ts @@ -18,7 +18,6 @@ import axiosInstance from "@/api/axiosInstance"; -import { AccountModel } from "@/features/accounts/data/schema"; import { PaginatedResponse } from ".."; export interface MinimalAccount { @@ -56,6 +55,59 @@ export interface MailboxBatchProgress { current_batch: number; } + + +type Encryption = 'Ssl' | 'StartTls' | 'None'; +type AuthType = 'Password' | 'OAuth2'; +type Unit = 'Days' | 'Months' | 'Years'; +type AccountType = 'IMAP' | 'NoSync'; +// Interface definitions +interface AuthConfig { + auth_type: AuthType; + password?: string; +} + +export interface ImapConfig { + host: string; + port: number; // integer, 0-65535 + encryption: Encryption; + auth: AuthConfig; + use_proxy?: number; +} + +interface RelativeDate { + unit: Unit; + value: number; // integer, minimum 1 +} + +interface DateSelection { + fixed?: string; // format: "YYYY-MM-DD" + relative?: RelativeDate; +} + +export interface AccountModel { + id: number; + account_type: AccountType; + imap?: ImapConfig; + enabled: boolean; + name?: string, + email: string; + capabilities?: string[]; + date_since?: DateSelection; + date_before?: RelativeDate; + folder_limit?: number, + sync_folders: string[]; + sync_interval_min?: number; + sync_batch_size?: number; + created_by: number; + created_user_name: string; + created_user_email: string; + created_at: number; + updated_at: number; + use_proxy?: number + use_dangerous: boolean +} + export const account_state = async (account_id: number) => { const response = await axiosInstance.get(`/api/v1/account-state/${account_id}`); return response.data; diff --git a/web/src/features/accounts/components/access-assignment-dialog.tsx b/web/src/features/accounts/components/access-assignment-dialog.tsx index 18a1817..2d9aac2 100644 --- a/web/src/features/accounts/components/access-assignment-dialog.tsx +++ b/web/src/features/accounts/components/access-assignment-dialog.tsx @@ -52,11 +52,9 @@ import { Button } from '@/components/ui/button' import { ScrollArea } from '@/components/ui/scroll-area' import { Input } from '@/components/ui/input' import { useToast } from '@/hooks/use-toast' - -import { AccountModel } from '../data/schema' import { useRoles } from '@/hooks/use-roles' import { useMinimalUsers } from '@/hooks/use-minimal-users' -import { access_assign } from '@/api/account/api' +import { access_assign, AccountModel } from '@/api/account/api' interface Props { currentRow: AccountModel diff --git a/web/src/features/accounts/components/account-detail.tsx b/web/src/features/accounts/components/account-detail.tsx index 0625b44..19285f8 100644 --- a/web/src/features/accounts/components/account-detail.tsx +++ b/web/src/features/accounts/components/account-detail.tsx @@ -17,7 +17,6 @@ // along with this program. If not, see . -import { AccountModel } from '../data/schema' import { ScrollArea } from '@/components/ui/scroll-area' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' @@ -25,6 +24,7 @@ import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { useTranslation } from 'react-i18next' +import { AccountModel } from '@/api/account/api' interface Props { open: boolean @@ -34,6 +34,30 @@ interface Props { export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) { const { t } = useTranslation() + + + + + const sinceText = (() => { + if (currentRow.date_since?.fixed) { + return currentRow.date_since.fixed; + } + + if (currentRow.date_since?.relative?.value) { + return `${t('accounts.sinceRelativeValue', { + value: currentRow.date_since!.relative!.value, + unit: t(`accounts.${currentRow.date_since!.relative!.unit!.toLowerCase()}`) + })}`; + } + + return t('accounts.syncAll'); + })(); + + const hasSince = !!currentRow.date_since; + const hasBefore = !!currentRow.date_before?.value; + + + return ( {t('accounts.incrementalSyncInterval')}: {t('accounts.everyMinutes', { minutes: currentRow.sync_interval_min })} +
+ {t('accounts.syncBatchSize')}: + {currentRow.sync_batch_size} +
{t('accounts.capabilities')}: @@ -84,14 +112,33 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
- {t('accounts.dateSelection')}: - - {currentRow.date_since?.fixed - ? t('accounts.since') + ' ' + currentRow.date_since.fixed - : currentRow.date_since?.relative - ? t('accounts.recent') + ' ' + currentRow.date_since.relative.value + ' ' + currentRow.date_since.relative.unit - : t('accounts.notAvailable')} - + {t('accounts.syncScope')}: + {hasSince && ( +
+ + {t('accounts.sinceFixed')}: + + {sinceText} +
+ )} + {hasBefore && ( +
+ + {t('accounts.beforeRelative')}: + + + {t('accounts.beforeRelativeValue', { + value: currentRow.date_before!.value, + unit: t(`accounts.${currentRow.date_before!.unit!.toLowerCase()}`) + })} + +
+ )} + {!hasSince && !hasBefore && ( + + {t('accounts.syncAll')} + + )}
{t('accounts.folderLimit')}: @@ -100,8 +147,6 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
- - {/* Server Configuration Card */} {t('accounts.serverConfiguration')} @@ -143,8 +188,6 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) { - - {/* Sync Folders Card */} {t('accounts.syncFoldersTitle')} diff --git a/web/src/features/accounts/components/action-dialog.tsx b/web/src/features/accounts/components/action-dialog.tsx index 3921700..b7b2183 100644 --- a/web/src/features/accounts/components/action-dialog.tsx +++ b/web/src/features/accounts/components/action-dialog.tsx @@ -16,14 +16,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - import { zodResolver } from '@hookform/resolvers/zod'; import * as React from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Form } from '@/components/ui/form'; -import { AccountModel, ImapConfig } from '../data/schema'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useToast } from '@/hooks/use-toast'; @@ -31,11 +29,12 @@ import Step1 from './step1'; import Step2 from './step2'; import Step3 from './step3'; import Step4 from './step4'; -import { create_account, autoconfig, update_account } from '@/api/account/api'; +import { create_account, autoconfig, update_account, AccountModel, ImapConfig } from '@/api/account/api'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ToastAction } from '@/components/ui/toast'; import { AxiosError } from 'axios'; import { useTranslation } from 'react-i18next'; +import { cn } from "@/lib/utils"; const encryptionSchema = z.union([ z.literal('Ssl'), @@ -80,7 +79,7 @@ const getRelativeDateSchema = (t: (key: string) => string) => z.object({ }); const getDateSelectionSchema = (t: (key: string) => string) => z.union([ - z.object({ fixed: z.string({ message: t('accounts.selectDate') }) },), + z.object({ fixed: z.string({ message: t('accounts.selectDate') }) }), z.object({ relative: getRelativeDateSchema(t) }), z.undefined(), ]); @@ -107,8 +106,13 @@ export type Account = { value?: number; }; }; + date_before?: { + unit?: 'Days' | 'Months' | 'Years'; + value?: number; + }; folder_limit?: number; sync_interval_min: number; + sync_batch_size: number; }; const getAccountSchema = (isEdit: boolean, t: (key: string) => string) => @@ -119,12 +123,18 @@ const getAccountSchema = (isEdit: boolean, t: (key: string) => string) => enabled: z.boolean(), use_dangerous: z.boolean(), date_since: getDateSelectionSchema(t).optional(), + date_before: getRelativeDateSchema(t).optional(), folder_limit: z .number({ invalid_type_error: t('validation.folderLimitMustBeNumber') }) .int() .min(100, { message: t('validation.folderLimitMustBeAtLeast100') }) .optional(), sync_interval_min: z.number({ invalid_type_error: t('validation.incrementalSyncMustBeNumber') }).int().min(10, { message: t('validation.incrementalSyncMustBeAtLeast10') }), + sync_batch_size: z + .number({ invalid_type_error: t('validation.incrementalSyncMustBeNumber') }) + .int() + .min(30, { message: t('validation.incrementalSyncMustBeAtLeast10') }) + .max(200, { message: t('validation.incrementalSyncMustBeAtLeast10') }), }); type Step = { @@ -133,14 +143,12 @@ type Step = { fields: (keyof Account)[]; }; -export type Steps = [ - ...Step[] -]; +export type Steps = [...Step[]]; const getSteps = (t: (key: string) => string): Steps => [ { id: "step-1", name: t('accounts.steps.emailAddress'), fields: ["email"] }, { id: "step-2", name: t('accounts.steps.imap'), fields: ["imap", "use_dangerous", "name"] }, - { id: "step-3", name: t('accounts.steps.syncPreferences'), fields: ["enabled", "date_since", "folder_limit", "sync_interval_min"] }, + { id: "step-3", name: t('accounts.steps.syncPreferences'), fields: ["enabled", "date_since", "date_before", "folder_limit", "sync_interval_min", "sync_batch_size"] }, { id: "step-4", name: t('accounts.steps.summary'), fields: [] }, ]; @@ -168,8 +176,10 @@ const defaultValues: Account = { enabled: true, use_dangerous: false, date_since: undefined, + date_before: undefined, folder_limit: undefined, sync_interval_min: 10, + sync_batch_size: 50, }; const emptyImap: ImapConfig = { @@ -194,8 +204,10 @@ const mapCurrentRowToFormValues = (currentRow: AccountModel): Account => { enabled: currentRow.enabled, use_dangerous: currentRow.use_dangerous, date_since: currentRow.date_since ?? undefined, + date_before: currentRow.date_before ?? undefined, folder_limit: currentRow.folder_limit ?? undefined, sync_interval_min: currentRow.sync_interval_min ?? 10, + sync_batch_size: currentRow.sync_batch_size ?? 50, }; }; @@ -272,8 +284,10 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) { enabled: data.enabled, use_dangerous: data.use_dangerous, date_since: data.date_since, + date_before: data.date_before, folder_limit: data.folder_limit, sync_interval_min: data.sync_interval_min, + sync_batch_size: data.sync_batch_size, }; if (isEdit) { updateMutation.mutate(commonData); @@ -330,61 +344,67 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) { { - form.reset(); - setCurrentStep(1); + if (!state) { + form.reset(); + setCurrentStep(1); + } onOpenChange(state); }} > - - - {isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')} - - {isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')} - {t('accounts.clickSaveWhenDone')} - - - - <> -
- {steps.map((step, index) => ( + +
+ + {isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')} + + {isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')} + {t('accounts.clickSaveWhenDone')} + + +
+ +
+
+ {steps.map((step, index) => ( +
- ))} -
-
-
-
- {steps.map((step, index) => ( -
- -
- {t('accounts.step', { index: index + 1 })} - {step.name} -
-
- ))} -
+ {step.name} +
+ ))} +
+
+ {steps.map((step, index) => ( +
+ +
+ {t('accounts.step', { index: index + 1 })} + + {step.name} + +
+
+ ))} +
+ +
+ +
- + {currentStep === 1 && } {currentStep === 2 && } {currentStep === 3 && } @@ -392,14 +412,16 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
-
- - - + +
+
+ + {currentStep > 1 && ( diff --git a/web/src/features/accounts/components/columns.tsx b/web/src/features/accounts/components/columns.tsx index 87e6c2c..4ebb3d1 100644 --- a/web/src/features/accounts/components/columns.tsx +++ b/web/src/features/accounts/components/columns.tsx @@ -19,8 +19,6 @@ import { ColumnDef } from '@tanstack/react-table' import LongText from '@/components/long-text' - -import { AccountModel } from '../data/schema' import { DataTableColumnHeader } from './data-table-column-header' import { DataTableRowActions } from './data-table-row-actions' import { format } from 'date-fns' @@ -28,6 +26,7 @@ import { OAuth2Action } from './oauth2-action' import { RunningStateCellAction } from './running-state-action' import { EnableAction } from './enable-action' import { useTranslation } from 'react-i18next' +import { AccountModel } from '@/api/account/api' export function useColumns(): ColumnDef[] { const { t } = useTranslation() @@ -112,7 +111,7 @@ export function useColumns(): ColumnDef[] { { accessorKey: 'created_by', header: ({ column }) => ( - + ), cell: ({ row }) => { const { created_user_name, created_user_email } = row.original; diff --git a/web/src/features/accounts/components/data-table-row-actions.tsx b/web/src/features/accounts/components/data-table-row-actions.tsx index 1052b77..89d4616 100644 --- a/web/src/features/accounts/components/data-table-row-actions.tsx +++ b/web/src/features/accounts/components/data-table-row-actions.tsx @@ -30,10 +30,10 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useAccountContext } from '../context' -import { AccountModel } from '../data/schema' import { Mailbox, MessageSquareMore } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useCurrentUser } from '@/hooks/use-current-user' +import { AccountModel } from '@/api/account/api' interface DataTableRowActionsProps { row: Row @@ -113,7 +113,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { setOpen('access-assign') }} > - Access Control + {t('accounts.accessControl')} diff --git a/web/src/features/accounts/components/delete-dialog.tsx b/web/src/features/accounts/components/delete-dialog.tsx index 98ac0ef..cb2e8a1 100644 --- a/web/src/features/accounts/components/delete-dialog.tsx +++ b/web/src/features/accounts/components/delete-dialog.tsx @@ -24,11 +24,10 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { ConfirmDialog } from '@/components/confirm-dialog' -import { AccountModel } from '../data/schema' import { useMutation, useQueryClient } from '@tanstack/react-query' import { ToastAction } from '@/components/ui/toast' import { AxiosError } from 'axios' -import { remove_account } from '@/api/account/api' +import { AccountModel, remove_account } from '@/api/account/api' import { useTranslation } from 'react-i18next' interface Props { diff --git a/web/src/features/accounts/components/enable-action.tsx b/web/src/features/accounts/components/enable-action.tsx index ce5f488..7004626 100644 --- a/web/src/features/accounts/components/enable-action.tsx +++ b/web/src/features/accounts/components/enable-action.tsx @@ -18,13 +18,12 @@ import { Row } from '@tanstack/react-table' -import { AccountModel } from '../data/schema' import { Switch } from '@/components/ui/switch' import { useState } from 'react' import { ConfirmDialog } from '@/components/confirm-dialog' import { ToastAction } from '@/components/ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { update_account } from '@/api/account/api' +import { AccountModel, update_account } from '@/api/account/api' import { toast } from '@/hooks/use-toast' import { AxiosError } from 'axios' import { useTranslation } from 'react-i18next' diff --git a/web/src/features/accounts/components/nosync-dialog.tsx b/web/src/features/accounts/components/nosync-dialog.tsx index 4f101d4..91fc8db 100644 --- a/web/src/features/accounts/components/nosync-dialog.tsx +++ b/web/src/features/accounts/components/nosync-dialog.tsx @@ -28,12 +28,11 @@ import { AxiosError } from 'axios'; import React from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { create_account, update_account } from '@/api/account/api'; +import { AccountModel, create_account, update_account } from '@/api/account/api'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { Loader2 } from 'lucide-react'; -import { AccountModel } from '../data/schema'; import { useTranslation } from 'react-i18next'; diff --git a/web/src/features/accounts/components/oauth2-action.tsx b/web/src/features/accounts/components/oauth2-action.tsx index 12ad8ce..0743bcc 100644 --- a/web/src/features/accounts/components/oauth2-action.tsx +++ b/web/src/features/accounts/components/oauth2-action.tsx @@ -20,11 +20,11 @@ import { Row } from '@tanstack/react-table' import { Button } from '@/components/ui/button' import { useAccountContext } from '../context' -import { AccountModel } from '../data/schema' import { useTranslation } from 'react-i18next' import { useCurrentUser } from '@/hooks/use-current-user' import { toast } from '@/hooks/use-toast' import { ToastAction } from '@/components/ui/toast' +import { AccountModel } from '@/api/account/api' interface DataTableRowActionsProps { row: Row diff --git a/web/src/features/accounts/components/oauth2-tokens.tsx b/web/src/features/accounts/components/oauth2-tokens.tsx index 5e5c4a6..148ab18 100644 --- a/web/src/features/accounts/components/oauth2-tokens.tsx +++ b/web/src/features/accounts/components/oauth2-tokens.tsx @@ -26,7 +26,6 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' -import { AccountModel } from '../data/schema' import { Button } from '@/components/ui/button' import { get_oauth2_tokens } from '@/api/oauth2/api' import { useQuery } from '@tanstack/react-query' @@ -44,6 +43,7 @@ import { ToastAction } from '@/components/ui/toast' import { useNavigate } from '@tanstack/react-router' import { dateFnsLocaleMap } from '@/lib/utils' import { enUS } from 'date-fns/locale' +import { AccountModel } from '@/api/account/api' interface Props { currentRow: AccountModel diff --git a/web/src/features/accounts/components/running-state-action.tsx b/web/src/features/accounts/components/running-state-action.tsx index efba329..9ed5f6f 100644 --- a/web/src/features/accounts/components/running-state-action.tsx +++ b/web/src/features/accounts/components/running-state-action.tsx @@ -19,12 +19,12 @@ import { Row } from '@tanstack/react-table' import { Button } from '@/components/ui/button' -import { AccountModel } from '../data/schema'; import { useAccountContext } from '../context'; import { useTranslation } from 'react-i18next'; import { useCurrentUser } from '@/hooks/use-current-user'; import { toast } from '@/hooks/use-toast'; import { ToastAction } from '@/components/ui/toast'; +import { AccountModel } from '@/api/account/api'; interface Props { row: Row diff --git a/web/src/features/accounts/components/running-state-dialog.tsx b/web/src/features/accounts/components/running-state-dialog.tsx index c23dd2d..16b6f36 100644 --- a/web/src/features/accounts/components/running-state-dialog.tsx +++ b/web/src/features/accounts/components/running-state-dialog.tsx @@ -25,9 +25,8 @@ import { DialogTitle, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' -import { AccountModel } from '../data/schema' import { useQuery } from '@tanstack/react-query' -import { account_state } from '@/api/account/api' +import { account_state, AccountModel } from '@/api/account/api' import { formatDistanceToNow, formatDuration, intervalToDuration } from 'date-fns' import { ScrollArea } from '@/components/ui/scroll-area' import { Skeleton } from '@/components/ui/skeleton' diff --git a/web/src/features/accounts/components/step3.tsx b/web/src/features/accounts/components/step3.tsx index c4df623..904c9e2 100644 --- a/web/src/features/accounts/components/step3.tsx +++ b/web/src/features/accounts/components/step3.tsx @@ -39,189 +39,206 @@ import { Button } from "@/components/ui/button"; import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; import { Calendar } from "@/components/ui/calendar"; -import { cn } from "@/lib/utils"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { cn, dateFnsLocaleMap } from "@/lib/utils"; import { useState } from "react"; import { Checkbox } from "@/components/ui/checkbox"; import { useTranslation } from "react-i18next"; +import { enUS } from "date-fns/locale"; +import i18n from "@/i18n"; + + +type SyncMode = 'all' | 'since_fixed' | 'since_relative' | 'before_relative'; export default function Step3() { const { t } = useTranslation(); const { control, getValues, setValue } = useFormContext(); const current = getValues(); - const [rangeType, setRangeType] = useState<'none' | 'fixed' | 'relative'>( - current.date_since ? (current.date_since.fixed ? 'fixed' : 'relative') : 'none' - ); + + const [syncMode, setSyncMode] = useState(() => { + if (current.date_before) return 'before_relative'; + if (current.date_since?.fixed) return 'since_fixed'; + if (current.date_since?.relative) return 'since_relative'; + return 'all'; + }); + + + const handleModeChange = (mode: SyncMode) => { + setSyncMode(mode); + + setValue("date_since", undefined); + setValue("date_before", undefined); + + if (mode === 'since_fixed') { + setValue("date_since.fixed", undefined); + } else if (mode === 'since_relative') { + setValue("date_since.relative", { value: 1, unit: 'Months' }); + } else if (mode === 'before_relative') { + setValue("date_before", { value: 1, unit: 'Years' }); + } + }; return (
- ( - - - {t('accounts.incrementalSync')}: - - - field.onChange(parseInt(e.target.value, 10))} - /> - - - - )} - /> +
+ ( + + {t('accounts.incrementalSync')} + + field.onChange(parseInt(e.target.value, 10))} /> + + + + {t('accounts.incrementalSyncDescription')} + + + )} + /> + ( + + {t('accounts.syncBatchSize')} + + field.onChange(parseInt(e.target.value, 10))} /> + + + + {t('accounts.syncBatchSizeDescription')} + + + )} + /> +
( - - {t('accounts.enabled')}: + - + - {t('accounts.enabledDescription')} +
+ {t('accounts.enabled')} + {t('accounts.enabledDescription')} +
)} /> - {t('accounts.dateSince')}: - { - setRangeType(value); - if (value === 'none') { - setValue("date_since", undefined, { shouldValidate: true }); - } - if (value === 'fixed') { - setValue("date_since", { fixed: undefined }, { shouldValidate: true }); - } - if (value === 'relative') { - setValue("date_since", { relative: { value: undefined, unit: undefined } }, { shouldValidate: true }); - } - }} - className="flex flex-row space-x-4" - > - - - {t('accounts.none')} - - - - {t('accounts.fixed')} - - - - {t('accounts.relative')} - - - - {t('accounts.syncStartDateDescription', { - fixedPart: rangeType === 'fixed' ? t('accounts.syncAfterDate') : t('accounts.syncRecentData'), - })} - +
+
+ + {t('accounts.syncScope', 'Sync Strategy')} + + {t('accounts.syncScopeDescription', 'Choose which emails should be indexed and archived.')} + + + +
+ {syncMode === 'since_fixed' && ( + { + const currentLang = i18n.language.toLowerCase().replace('_', '-'); + const dateLocale = dateFnsLocaleMap[currentLang] || enUS; + return + {t('accounts.selectDate')} + + + + + + + + field.onChange(date?.toLocaleDateString('en-CA'))} + disabled={(date) => date > new Date() || date < new Date("1900-01-01")} + locale={dateLocale} + initialFocus + /> + + + + ; - {rangeType === 'fixed' && ( - ( - - - - - - - - - { - if (value) { - const formattedDate = value.toLocaleDateString('en-CA'); - field.onChange(formattedDate); - } else { - field.onChange(null); - } - }} - disabled={(date) => date > new Date() || date < new Date("1900-01-01")} - initialFocus - /> - - - - + }} + /> )} - /> - )} - {rangeType === 'relative' && ( -
-
- ( - - - field.onChange(parseInt(e.target.value, 10))} /> - - - - )} - /> -
-
- ( - - field.onChange(parseInt(e.target.value, 10))} /> - - {t('accounts.days')} - {t('accounts.months')} - {t('accounts.years')} - - - - - )} - /> -
+ + + )} + /> + ( + + {t('accounts.unit', 'Unit')} + + + + )} + /> +
+ )}
- )} +
+ +
( - {t('accounts.folderLimit')}: + {t('accounts.folderLimit')} {t('accounts.folderLimitDescription')}
); -} +} \ No newline at end of file diff --git a/web/src/features/accounts/components/step4.tsx b/web/src/features/accounts/components/step4.tsx index b1f3b65..dedaf61 100644 --- a/web/src/features/accounts/components/step4.tsx +++ b/web/src/features/accounts/components/step4.tsx @@ -27,9 +27,28 @@ export default function Step4() { const { getValues } = useFormContext(); const summaryData = getValues(); + + const sinceText = (() => { + if (summaryData.date_since?.fixed) { + return summaryData.date_since.fixed; + } + + if (summaryData.date_since?.relative?.value) { + return `${t('accounts.sinceRelativeValue', { + value: summaryData.date_since!.relative!.value, + unit: t(`accounts.${summaryData.date_since!.relative!.unit!.toLowerCase()}`) + })}`; + } + + return t('accounts.syncAll'); + })(); + + const hasSince = !!summaryData.date_since; + const hasBefore = !!summaryData.date_before?.value; + return (
- + {t('accounts.email')}: {summaryData.email} @@ -82,17 +101,44 @@ export default function Step4() { - - {t('accounts.dateSelection')}: - - {summaryData.date_since?.fixed - ? t('accounts.since') + ' ' + summaryData.date_since.fixed - : summaryData.date_since?.relative && summaryData.date_since.relative.value && summaryData.date_since.relative.unit - ? t('accounts.recent') + ' ' + summaryData.date_since.relative.value + ' ' + summaryData.date_since.relative.unit - : t('accounts.notAvailable')} + + + {t('accounts.syncScope')}: + + + + {hasSince && ( +
+ + {t('accounts.sinceFixed')}: + + {sinceText} +
+ )} + + {hasBefore && ( +
+ + {t('accounts.beforeRelative')}: + + + {t('accounts.beforeRelativeValue', { + value: summaryData.date_before!.value, + unit: t(`accounts.${summaryData.date_before!.unit!.toLowerCase()}`) + })} + +
+ )} + + {!hasSince && !hasBefore && ( + + {t('accounts.syncAll')} + + )}
+ {t('accounts.folderLimit')}: {summaryData.folder_limit ?? t('accounts.notAvailable')} @@ -102,6 +148,11 @@ export default function Step4() { {t('accounts.incrementalSync')}: {summaryData.sync_interval_min} {t('accounts.minutes')} + + + {t('accounts.syncBatchSize')}: + {summaryData.sync_batch_size} +
); diff --git a/web/src/features/accounts/components/sync-folders.tsx b/web/src/features/accounts/components/sync-folders.tsx index 8fdebc3..61592e3 100644 --- a/web/src/features/accounts/components/sync-folders.tsx +++ b/web/src/features/accounts/components/sync-folders.tsx @@ -29,12 +29,11 @@ import { Button } from '@/components/ui/button' import { useMutation, useQueryClient } from '@tanstack/react-query' import { Loader2, CheckSquare, Square } from 'lucide-react' import { useCallback, useEffect, useMemo, useState } from 'react' -import { AccountModel } from '../data/schema' import { toast } from '@/hooks/use-toast' import { list_mailboxes, MailboxData } from '@/api/mailbox/api' import { buildTree, ExtendedTreeItemProps } from '@/lib/build-tree' import { Skeleton } from '@/components/ui/skeleton' -import { update_account } from '@/api/account/api' +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' diff --git a/web/src/features/accounts/components/table.tsx b/web/src/features/accounts/components/table.tsx index 09587e7..efaa100 100644 --- a/web/src/features/accounts/components/table.tsx +++ b/web/src/features/accounts/components/table.tsx @@ -41,10 +41,10 @@ import { TableHeader, TableRow, } from '@/components/ui/table' -import { AccountModel } from '../data/schema' import { DataTablePagination } from './data-table-pagination' import { DataTableToolbar } from './data-table-toolbar' import { useTranslation } from 'react-i18next' +import { AccountModel } from '@/api/account/api' declare module '@tanstack/react-table' { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/src/features/accounts/context/index.tsx b/web/src/features/accounts/context/index.tsx index e059428..3ca3073 100644 --- a/web/src/features/accounts/context/index.tsx +++ b/web/src/features/accounts/context/index.tsx @@ -17,8 +17,8 @@ // along with this program. If not, see . +import { AccountModel } from '@/api/account/api'; import React from 'react' -import { AccountModel } from '../data/schema' export type AccountDialogType = | 'add-imap' diff --git a/web/src/features/accounts/data/schema.ts b/web/src/features/accounts/data/schema.ts deleted file mode 100644 index 1965140..0000000 --- a/web/src/features/accounts/data/schema.ts +++ /dev/null @@ -1,67 +0,0 @@ -// -// Copyright (c) 2025 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 . - - -type Encryption = 'Ssl' | 'StartTls' | 'None'; -type AuthType = 'Password' | 'OAuth2'; -type Unit = 'Days' | 'Months' | 'Years'; -type AccountType = 'IMAP' | 'NoSync'; -// Interface definitions -interface AuthConfig { - auth_type: AuthType; - password?: string; -} - -export interface ImapConfig { - host: string; - port: number; // integer, 0-65535 - encryption: Encryption; - auth: AuthConfig; - use_proxy?: number; -} - -interface RelativeDate { - unit: Unit; - value: number; // integer, minimum 1 -} - -interface DateSelection { - fixed?: string; // format: "YYYY-MM-DD" - relative?: RelativeDate; -} - -export interface AccountModel { - id: number; - account_type: AccountType; - imap?: ImapConfig; - enabled: boolean; - name?: string, - email: string; - capabilities?: string[]; - date_since?: DateSelection; - folder_limit?: number, - sync_folders: string[]; - sync_interval_min?: number; - created_by: number; - created_user_name: string; - created_user_email: string; - created_at: number; - updated_at: number; - use_proxy?: number - use_dangerous: boolean -} \ No newline at end of file diff --git a/web/src/features/accounts/index.tsx b/web/src/features/accounts/index.tsx index 11feb4f..b70905e 100644 --- a/web/src/features/accounts/index.tsx +++ b/web/src/features/accounts/index.tsx @@ -30,9 +30,8 @@ import AccountProvider, { } from './context' import { MoreVertical, Plus } from 'lucide-react' import Logo from '@/assets/logo.svg' -import { AccountModel } from './data/schema' import { AccountDetailDrawer } from './components/account-detail' -import { list_accounts } from '@/api/account/api' +import { AccountModel, list_accounts } from '@/api/account/api' import { TableSkeleton } from '@/components/table-skeleton' import { useQuery } from '@tanstack/react-query' import { OAuth2TokensDialog } from './components/oauth2-tokens' diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json index 68e5602..6204542 100644 --- a/web/src/locales/ar.json +++ b/web/src/locales/ar.json @@ -138,6 +138,22 @@ "systemVersion": "إصدار النظام" }, "accounts": { + "beforeRelativeValue": "مزامنة رسائل البريد الإلكتروني قبل {{value}} {{unit}} مضت", + "sinceRelativeValue": "مزامنة رسائل البريد الإلكتروني لآخر {{value}} {{unit}}", + "syncBatchSize": "حجم دفعة المزامنة", + "syncBatchSizeDescription": "عدد الرسائل التي يتم جلبها لكل طلب IMAP", + "incrementalSyncDescription": "عدد مرات إجراء مزامنة البريد الإلكتروني المتزايدة (بالدقائق)", + "syncScope": "استراتيجية المزامنة", + "syncScopeDescription": "اختر رسائل البريد الإلكتروني التي يجب فهرستها وأرشفتها.", + "selectMode": "حدد وضع التصفية", + "syncAll": "مزامنة جميع رسائل البريد الإلكتروني", + "sinceFixed": "منذ تاريخ محدد", + "sinceRelative": "مزامنة رسائل البريد الإلكتروني الحديثة فقط", + "beforeRelative": "أرشفة رسائل البريد الإلكتروني القديمة فقط", + "duration": "المدة", + "unit": "الوحدة", + "accessControl": "التحكم في الوصول", + "owner": "المنشئ", "access_control": { "title": "تخصيص الوصول للحساب", "description": "تعيين الأدوار والمستخدمين المفوضين لـ {{email}}", diff --git a/web/src/locales/da.json b/web/src/locales/da.json index 00661cc..754fa5b 100644 --- a/web/src/locales/da.json +++ b/web/src/locales/da.json @@ -138,6 +138,22 @@ "systemVersion": "Systemversion" }, "accounts": { + "beforeRelativeValue": "Synkroniser e-mails fra før {{value}} {{unit}} siden", + "sinceRelativeValue": "Synkroniser e-mails fra de seneste {{value}} {{unit}}", + "syncBatchSize": "Synkroniseringsbatchstørrelse", + "syncBatchSizeDescription": "Antal beskeder hentet per IMAP-forespørgsel", + "incrementalSyncDescription": "Hvor ofte inkrementel e-mail-synkronisering udføres (i minutter)", + "syncScope": "Synkroniseringsstrategi", + "syncScopeDescription": "Vælg hvilke e-mails der skal indekseres og arkiveres.", + "selectMode": "Vælg filtertilstand", + "syncAll": "Synkroniser alle e-mails", + "sinceFixed": "Siden en bestemt dato", + "sinceRelative": "Synkroniser kun nylige e-mails", + "beforeRelative": "Arkiver kun gamle e-mails", + "duration": "Varighed", + "unit": "Enhed", + "accessControl": "Adgangskontrol", + "owner": "Oprettet af", "access_control": { "title": "Tildeling af kontoadgang", "description": "Tildel roller og autoriserede brugere til {{email}}", diff --git a/web/src/locales/de.json b/web/src/locales/de.json index fdb256d..6a6604c 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -138,6 +138,22 @@ "systemVersion": "Systemversion" }, "accounts": { + "beforeRelativeValue": "E-Mails synchronisieren, die älter als {{value}} {{unit}} sind", + "sinceRelativeValue": "E-Mails der letzten {{value}} {{unit}} synchronisieren", + "syncBatchSize": "Synchronisations-Batch-Größe", + "syncBatchSizeDescription": "Anzahl der pro IMAP-Anfrage abgerufenen Nachrichten", + "incrementalSyncDescription": "Häufigkeit der inkrementellen E-Mail-Synchronisierung (in Minuten)", + "syncScope": "Synchronisationsstrategie", + "syncScopeDescription": "Wählen Sie aus, welche E-Mails indiziert und archiviert werden sollen.", + "selectMode": "Filtermodus auswählen", + "syncAll": "Alle E-Mails synchronisieren", + "sinceFixed": "Seit einem bestimmten Datum", + "sinceRelative": "Nur aktuelle E-Mails synchronisieren", + "beforeRelative": "Nur alte E-Mails archivieren", + "duration": "Dauer", + "unit": "Einheit", + "accessControl": "Zugriffskontrolle", + "owner": "Ersteller", "access_control": { "title": "Kontozugriffszuweisung", "description": "Rollen und autorisierte Benutzer für {{email}} zuweisen", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index a7c9c8a..e69ba6a 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -138,6 +138,22 @@ "systemVersion": "System Version" }, "accounts": { + "beforeRelativeValue": "Sync emails before {{value}} {{unit}} ago", + "sinceRelativeValue": "Sync emails from the last", + "syncBatchSize": "Sync batch size", + "syncBatchSizeDescription": "Number of messages fetched per IMAP request", + "incrementalSyncDescription": "How often incremental email synchronization is performed (in minutes)", + "syncScopeDescription": "Choose which emails should be indexed and archived.", + "syncScope": "Sync Strategy", + "selectMode": "Select filter mode", + "syncAll": "Sync All Emails", + "sinceFixed": "Since Specific Date", + "sinceRelative": "Sync Recent Emails Only", + "beforeRelative": "Archive Old Emails Only", + "duration": "Duration", + "unit": "Unit", + "accessControl": "Access Control", + "owner": "Creator", "access_control": { "title": "Account Access Assignment", "description": "Assign roles and authorized users to {{email}}", diff --git a/web/src/locales/es.json b/web/src/locales/es.json index 35bd073..f2aa5b2 100644 --- a/web/src/locales/es.json +++ b/web/src/locales/es.json @@ -138,6 +138,22 @@ "systemVersion": "Versión del sistema" }, "accounts": { + "beforeRelativeValue": "Sincronizar correos de hace más de {{value}} {{unit}}", + "sinceRelativeValue": "Sincronizar correos de los últimos {{value}} {{unit}}", + "syncBatchSize": "Tamaño del lote de sincronización", + "syncBatchSizeDescription": "Número de mensajes obtenidos por solicitud IMAP", + "incrementalSyncDescription": "Frecuencia de sincronización incremental (en minutos)", + "syncScope": "Estrategia de sincronización", + "syncScopeDescription": "Elija qué correos deben indexarse y archivarse.", + "selectMode": "Seleccionar modo de filtro", + "syncAll": "Sincronizar todos los correos", + "sinceFixed": "Desde una fecha específica", + "sinceRelative": "Sincronizar solo correos recientes", + "beforeRelative": "Archivar solo correos antiguos", + "duration": "Duración", + "unit": "Unidad", + "accessControl": "Control de acceso", + "owner": "Creador", "access_control": { "title": "Asignación de Acceso a la Cuenta", "description": "Asignar roles y usuarios autorizados a {{email}}", diff --git a/web/src/locales/fi.json b/web/src/locales/fi.json index 2de8161..b579b5b 100644 --- a/web/src/locales/fi.json +++ b/web/src/locales/fi.json @@ -138,6 +138,22 @@ "systemVersion": "Järjestelmäversio" }, "accounts": { + "beforeRelativeValue": "Synkronoi sähköpostit, jotka ovat vanhempia kuin {{value}} {{unit}}", + "sinceRelativeValue": "Synkronoi viimeisimmän {{value}} {{unit}} sähköpostit", + "syncBatchSize": "Synkronoinnin eräkoko", + "syncBatchSizeDescription": "Per IMAP-pyyntö noudettujen viestien määrä", + "incrementalSyncDescription": "Kuinka usein inkrementaalinen sähköpostin synkronointi suoritetaan (minuutteina)", + "syncScope": "Synkronointistrategia", + "syncScopeDescription": "Valitse mitkä sähköpostit indeksoidaan ja arkistoidaan.", + "selectMode": "Valitse suodatustila", + "syncAll": "Synkronoi kaikki sähköpostit", + "sinceFixed": "Tietystä päivämäärästä lähtien", + "sinceRelative": "Synkronoi vain viimeisimmät sähköpostit", + "beforeRelative": "Arkistoi vain vanhat sähköpostit", + "duration": "Kesto", + "unit": "Yksikkö", + "accessControl": "Pääsynhallinta", + "owner": "Luoja", "access_control": { "title": "Tilin pääsynhallinta", "description": "Määritä roolit ja valtuutetut käyttäjät kohteelle {{email}}", diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json index d413b46..0f6daff 100644 --- a/web/src/locales/fr.json +++ b/web/src/locales/fr.json @@ -138,6 +138,22 @@ "systemVersion": "Version du système" }, "accounts": { + "beforeRelativeValue": "Synchroniser les e-mails datant de plus de {{value}} {{unit}}", + "sinceRelativeValue": "Synchroniser les e-mails des derniers {{value}} {{unit}}", + "syncBatchSize": "Taille du lot de synchronisation", + "syncBatchSizeDescription": "Nombre de messages récupérés par requête IMAP", + "incrementalSyncDescription": "Fréquence de synchronisation incrémentielle (en minutes)", + "syncScope": "Stratégie de synchronisation", + "syncScopeDescription": "Choisissez les e-mails à indexer et à archiver.", + "selectMode": "Sélectionner le mode de filtrage", + "syncAll": "Synchroniser tous les e-mails", + "sinceFixed": "Depuis une date spécifique", + "sinceRelative": "Synchroniser uniquement les e-mails récents", + "beforeRelative": "Archiver uniquement les anciens e-mails", + "duration": "Durée", + "unit": "Unité", + "accessControl": "Contrôle d'accès", + "owner": "Créateur", "access_control": { "title": "Attribution d'accès au compte", "description": "Attribuer des rôles et des utilisateurs autorisés à {{email}}", diff --git a/web/src/locales/it.json b/web/src/locales/it.json index bcc83f8..443fa72 100644 --- a/web/src/locales/it.json +++ b/web/src/locales/it.json @@ -138,6 +138,22 @@ "systemVersion": "Versione del sistema" }, "accounts": { + "beforeRelativeValue": "Sincronizza le email antecedenti a {{value}} {{unit}} fa", + "sinceRelativeValue": "Sincronizza le email degli ultimi {{value}} {{unit}}", + "syncBatchSize": "Dimensione batch di sincronizzazione", + "syncBatchSizeDescription": "Numero di messaggi recuperati per richiesta IMAP", + "incrementalSyncDescription": "Frequenza della sincronizzazione incrementale (in minuti)", + "syncScope": "Strategia di sincronizzazione", + "syncScopeDescription": "Scegli quali email indicizzare e archiviare.", + "selectMode": "Seleziona modalità filtro", + "syncAll": "Sincronizza tutte le email", + "sinceFixed": "Da una data specifica", + "sinceRelative": "Sincronizza solo email recenti", + "beforeRelative": "Archivia solo email vecchie", + "duration": "Durata", + "unit": "Unità", + "accessControl": "Controllo accessi", + "owner": "Creatore", "access_control": { "title": "Assegnazione Accesso Account", "description": "Assegna ruoli e utenti autorizzati a {{email}}", diff --git a/web/src/locales/jp.json b/web/src/locales/jp.json index 3323558..1673655 100644 --- a/web/src/locales/jp.json +++ b/web/src/locales/jp.json @@ -138,6 +138,22 @@ "systemVersion": "システムバージョン" }, "accounts": { + "beforeRelativeValue": "{{value}} {{unit}} 前より前のメールを同期", + "sinceRelativeValue": "過去 {{value}} {{unit}} 分のメールを同期", + "syncBatchSize": "同期バッチサイズ", + "syncBatchSizeDescription": "1回のIMAPリクエストで取得するメッセージ数", + "incrementalSyncDescription": "増分メール同期の実行頻度(分単位)", + "syncScope": "同期戦略", + "syncScopeDescription": "インデックスを作成し、アーカイブするメールを選択します。", + "selectMode": "フィルタモードを選択", + "syncAll": "すべてのメールを同期", + "sinceFixed": "指定した日付以降", + "sinceRelative": "最近のメールのみ同期", + "beforeRelative": "古いメールのみアーカイブ", + "duration": "期間", + "unit": "単位", + "accessControl": "アクセス制御", + "owner": "作成者", "access_control": { "title": "アカウントアクセス割り当て", "description": "{{email}} にロールと権限ユーザーを割り当てます", diff --git a/web/src/locales/ko.json b/web/src/locales/ko.json index 69fcb4d..3bfc8ff 100644 --- a/web/src/locales/ko.json +++ b/web/src/locales/ko.json @@ -138,6 +138,22 @@ "systemVersion": "시스템 버전" }, "accounts": { + "beforeRelativeValue": "{{value}} {{unit}} 전 이전 이메일 동기화", + "sinceRelativeValue": "지난 {{value}} {{unit}} 동안의 이메일 동기화", + "syncBatchSize": "동기화 배치 크기", + "syncBatchSizeDescription": "IMAP 요청당 가져올 메시지 수", + "incrementalSyncDescription": "증분 이메일 동기화 수행 빈도 (분 단위)", + "syncScope": "동기화 전략", + "syncScopeDescription": "인덱싱 및 아카이빙할 이메일을 선택하십시오.", + "selectMode": "필터 모드 선택", + "syncAll": "모든 이메일 동기화", + "sinceFixed": "특정 날짜 이후", + "sinceRelative": "최신 이메일만 동기화", + "beforeRelative": "오래된 이메일만 아카이브", + "duration": "기간", + "unit": "단위", + "accessControl": "액세스 제어", + "owner": "생성자", "access_control": { "title": "계정 액세스 할당", "description": "{{email}}에 역할 및 권한 사용자를 할당합니다", diff --git a/web/src/locales/nl.json b/web/src/locales/nl.json index a2e39af..d63417e 100644 --- a/web/src/locales/nl.json +++ b/web/src/locales/nl.json @@ -138,6 +138,22 @@ "systemVersion": "Systeemversie" }, "accounts": { + "beforeRelativeValue": "Synchroniseer e-mails van vóór {{value}} {{unit}} geleden", + "sinceRelativeValue": "Synchroniseer e-mails van de afgelopen {{value}} {{unit}}", + "syncBatchSize": "Batchgrootte synchronisatie", + "syncBatchSizeDescription": "Aantal berichten opgehaald per IMAP-verzoek", + "incrementalSyncDescription": "Frequentie van incrementele synchronisatie (in minuten)", + "syncScope": "Synchronisatiestrategie", + "syncScopeDescription": "Kies welke e-mails geïndexeerd en gearchiveerd moeten worden.", + "selectMode": "Filtermodus selecteren", + "syncAll": "Alle e-mails synchroniseren", + "sinceFixed": "Sinds een specifieke datum", + "sinceRelative": "Alleen recente e-mails synchroniseren", + "beforeRelative": "Alleen oude e-mails archiveren", + "duration": "Duur", + "unit": "Eenheid", + "accessControl": "Toegangsbeheer", + "owner": "Maker", "access_control": { "title": "Toewijzing accounttoegang", "description": "Rollen en geautoriseerde gebruikers toewijzen aan {{email}}", diff --git a/web/src/locales/no.json b/web/src/locales/no.json index 5d17c7d..03191c0 100644 --- a/web/src/locales/no.json +++ b/web/src/locales/no.json @@ -138,6 +138,22 @@ "systemVersion": "Systemversjon" }, "accounts": { + "beforeRelativeValue": "Synkroniser e-poster fra før {{value}} {{unit}} siden", + "sinceRelativeValue": "Synkroniser e-poster fra de siste {{value}} {{unit}}", + "syncBatchSize": "Synkroniserings-batchstørrelse", + "syncBatchSizeDescription": "Antall meldinger hentet per IMAP-forespørsel", + "incrementalSyncDescription": "Hvor ofte inkrementell e-post-synkronisering utføres (i minutter)", + "syncScope": "Synkroniseringsstrategi", + "syncScopeDescription": "Velg hvilke e-poster som skal indekseres og arkiveres.", + "selectMode": "Velg filtermodus", + "syncAll": "Synkroniser alle e-poster", + "sinceFixed": "Siden spesifikk dato", + "sinceRelative": "Synkroniser kun nylige e-poster", + "beforeRelative": "Arkiver kun gamle e-poster", + "duration": "Varighet", + "unit": "Enhet", + "accessControl": "Tilgangskontroll", + "owner": "Opprettet av", "access_control": { "title": "Tildeling av kontotilgang", "description": "Tildel roller og autoriserte brukere til {{email}}", diff --git a/web/src/locales/pl.json b/web/src/locales/pl.json index 4270e9b..3d3f83d 100644 --- a/web/src/locales/pl.json +++ b/web/src/locales/pl.json @@ -138,6 +138,22 @@ "systemVersion": "Wersja systemu" }, "accounts": { + "beforeRelativeValue": "Synchronizuj wiadomości sprzed {{value}} {{unit}}", + "sinceRelativeValue": "Synchronizuj wiadomości z ostatnich {{value}} {{unit}}", + "syncBatchSize": "Rozmiar partii synchronizacji", + "syncBatchSizeDescription": "Liczba wiadomości pobieranych w jednym żądaniu IMAP", + "incrementalSyncDescription": "Częstotliwość wykonywania przyrostowej synchronizacji e-mail (w minutach)", + "syncScope": "Strategia synchronizacji", + "syncScopeDescription": "Wybierz wiadomości e-mail, które mają być indeksowane i archiwizowane.", + "selectMode": "Wybierz tryb filtrowania", + "syncAll": "Synchronizuj wszystkie wiadomości", + "sinceFixed": "Od określonej daty", + "sinceRelative": "Synchronizuj tylko ostatnie wiadomości", + "beforeRelative": "Archiwizuj tylko stare wiadomości", + "duration": "Czas trwania", + "unit": "Jednostka", + "accessControl": "Kontrola dostępu", + "owner": "Twórca", "access_control": { "title": "Przypisywanie dostępu do konta", "description": "Przypisz role i uprawnionych użytkowników dla {{email}}", diff --git a/web/src/locales/pt.json b/web/src/locales/pt.json index ddce6c8..9823905 100644 --- a/web/src/locales/pt.json +++ b/web/src/locales/pt.json @@ -138,6 +138,22 @@ "systemVersion": "Versão do sistema" }, "accounts": { + "beforeRelativeValue": "Sincronizar e-mails de antes de {{value}} {{unit}} atrás", + "sinceRelativeValue": "Sincronizar e-mails dos últimos {{value}} {{unit}}", + "syncBatchSize": "Tamanho do lote de sincronização", + "syncBatchSizeDescription": "Número de mensagens obtidas por solicitação IMAP", + "incrementalSyncDescription": "Frequência da sincronização incremental (em minutos)", + "syncScope": "Estratégia de sincronização", + "syncScopeDescription": "Escolha quais e-mails devem ser indexados e arquivados.", + "selectMode": "Selecionar modo de filtro", + "syncAll": "Sincronizar todos os e-mails", + "sinceFixed": "Desde uma data específica", + "sinceRelative": "Sincronizar apenas e-mails recentes", + "beforeRelative": "Arquivar apenas e-mails antigos", + "duration": "Duração", + "unit": "Unidade", + "accessControl": "Controle de acesso", + "owner": "Criador", "access_control": { "title": "Atribuição de Acesso à Conta", "description": "Atribuir funções e usuários autorizados a {{email}}", diff --git a/web/src/locales/ru.json b/web/src/locales/ru.json index d9193e2..ca1ad13 100644 --- a/web/src/locales/ru.json +++ b/web/src/locales/ru.json @@ -138,6 +138,22 @@ "systemVersion": "Версия системы" }, "accounts": { + "beforeRelativeValue": "Синхронизировать письма старее, чем {{value}} {{unit}} назад", + "sinceRelativeValue": "Синхронизировать письма за последние {{value}} {{unit}}", + "syncBatchSize": "Размер пакета синхронизации", + "syncBatchSizeDescription": "Количество сообщений, получаемых за один запрос IMAP", + "incrementalSyncDescription": "Частота инкрементной синхронизации почты (в минутах)", + "syncScope": "Стратегия синхронизации", + "syncScopeDescription": "Выберите письма для индексации и архивации.", + "selectMode": "Выберите режим фильтрации", + "syncAll": "Синхронизировать все письма", + "sinceFixed": "С определенной даты", + "sinceRelative": "Синхронизировать только новые письма", + "beforeRelative": "Архивировать только старые письма", + "duration": "Продолжительность", + "unit": "Единица", + "accessControl": "Контроль доступа", + "owner": "Создатель", "access_control": { "title": "Назначение доступа к аккаунту", "description": "Назначить роли и авторизованных пользователей для {{email}}", diff --git a/web/src/locales/sv.json b/web/src/locales/sv.json index 0debbb2..a669409 100644 --- a/web/src/locales/sv.json +++ b/web/src/locales/sv.json @@ -138,6 +138,22 @@ "systemVersion": "Systemversion" }, "accounts": { + "beforeRelativeValue": "Synkronisera mejl från före {{value}} {{unit}} sedan", + "sinceRelativeValue": "Synkronisera mejl från de senaste {{value}} {{unit}}", + "syncBatchSize": "Batchstorlek för synk", + "syncBatchSizeDescription": "Antal meddelanden som hämtas per IMAP-förfrågan", + "incrementalSyncDescription": "Hur ofta inkrementell e-post-synkronisering utförs (i minuter)", + "syncScope": "Synkstrategi", + "syncScopeDescription": "Välj vilka e-postmeddelanden som ska indexeras och arkiveras.", + "selectMode": "Välj filterläge", + "syncAll": "Synkronisera alla mejl", + "sinceFixed": "Sedan ett specifikt datum", + "sinceRelative": "Synka endast nyligen inkomna mejl", + "beforeRelative": "Arkivera endast gamla mejl", + "duration": "Varaktighet", + "unit": "Enhet", + "accessControl": "Åtkomstkontroll", + "owner": "Skapad av", "access_control": { "title": "Tilldelning av kontotillgång", "description": "Tilldela roller och auktoriserade användare till {{email}}", diff --git a/web/src/locales/zh-tw.json b/web/src/locales/zh-tw.json index ac50b31..d046339 100644 --- a/web/src/locales/zh-tw.json +++ b/web/src/locales/zh-tw.json @@ -138,6 +138,22 @@ "systemVersion": "系統版本" }, "accounts": { + "beforeRelativeValue": "同步 {{value}} {{unit}} 之前的郵件", + "sinceRelativeValue": "同步最近 {{value}} {{unit}} 內的郵件", + "syncBatchSize": "批次同步數量", + "syncBatchSizeDescription": "每次 IMAP 請求獲取的郵件數量", + "incrementalSyncDescription": "執行增量郵件同步的頻率(分鐘)", + "syncScope": "同步策略", + "syncScopeDescription": "選擇哪些郵件需要被索引和歸檔。", + "selectMode": "選擇過濾模式", + "syncAll": "同步所有郵件", + "sinceFixed": "從特定日期開始 (至今)", + "sinceRelative": "僅同步最近的郵件", + "beforeRelative": "僅封存舊郵件", + "duration": "時長", + "unit": "單位", + "accessControl": "訪問控制", + "owner": "建立者", "access_control": { "title": "帳戶訪問分配", "description": "為 {{email}} 分配角色和授權用戶", diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index b27a77f..d4fdaba 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -138,6 +138,22 @@ "systemVersion": "系统版本" }, "accounts": { + "beforeRelativeValue": "同步 {{value}} {{unit}} 之前的邮件", + "sinceRelativeValue": "同步最近 {{value}} {{unit}} 内的邮件", + "syncBatchSize": "批次同步数量", + "syncBatchSizeDescription": "每次 IMAP 请求获取的邮件数量", + "incrementalSyncDescription": "执行增量邮件同步的频率(分钟)", + "syncScope": "同步策略", + "syncScopeDescription": "选择哪些邮件需要被索引和归档。", + "selectMode": "选择过滤模式", + "syncAll": "同步所有邮件", + "sinceFixed": "从特定日期开始 (至今)", + "sinceRelative": "仅同步最近的邮件 (相对时间)", + "beforeRelative": "仅同步旧邮件", + "duration": "时长", + "unit": "单位", + "accessControl": "访问控制", + "owner": "创建者", "access_control": { "title": "账户访问分配", "description": "为 {{email}} 分配角色和授权用户",