diff --git a/crates/core/src/account/view.rs b/crates/core/src/account/view.rs index 5703760..6d9d9c7 100644 --- a/crates/core/src/account/view.rs +++ b/crates/core/src/account/view.rs @@ -22,7 +22,7 @@ use serde::{Deserialize, Serialize}; use crate::{ account::{ entity::ImapConfig, - migration::{AccountModel, AccountType, QuotaWindow}, + migration::{AccountModel, AccountType, ArchiveRules, QuotaWindow}, since::{DateSince, RelativeDate}, }, users::UserModel, @@ -58,6 +58,7 @@ pub struct AccountResp { pub imap_quota_window: Option, pub auto_download_new_mailboxes: Option, pub download_schedule: Option, + pub archive_rules: Option, pub deleting: bool, } @@ -96,6 +97,7 @@ impl AccountResp { imap_quota_window: account.imap_quota_window, auto_download_new_mailboxes: account.auto_download_new_mailboxes, download_schedule: account.download_schedule, + archive_rules: account.archive_rules, deleting: account.deleting, } } diff --git a/web/src/api/account/api.ts b/web/src/api/account/api.ts index 735fef4..f3e4eb7 100644 --- a/web/src/api/account/api.ts +++ b/web/src/api/account/api.ts @@ -118,6 +118,20 @@ interface DateSelection { export type QuotaWindow = 'hourly' | 'daily' | 'weekly' | 'monthly' + +export interface FilterRule { + include: string[]; + exclude: string[]; +} + +export interface ArchiveRules { + enabled: boolean; + senders: FilterRule; + subjects: FilterRule; + skip_larger_than?: number; + spam_headers: string[]; +} + export interface AccountModel { id: number; account_type: AccountType; @@ -145,6 +159,7 @@ export interface AccountModel { imap_quota_bytes?: number; auto_download_new_mailboxes?: boolean; download_schedule?: string; + archive_rules?: ArchiveRules; deleting?: boolean; } diff --git a/web/src/components/ui/breadcrumb.tsx b/web/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..6f31b63 --- /dev/null +++ b/web/src/components/ui/breadcrumb.tsx @@ -0,0 +1,67 @@ +// +// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { ChevronRight, Home } from "lucide-react"; +import { Link } from "@tanstack/react-router"; +import { cn } from "@/lib/utils"; + +export interface BreadcrumbItem { + label: string; + to?: string; +} + +interface BreadcrumbProps { + items: BreadcrumbItem[]; + className?: string; +} + +export function Breadcrumb({ items, className }: BreadcrumbProps) { + return ( + + ); +} diff --git a/web/src/features/accounts/account-new.tsx b/web/src/features/accounts/account-new.tsx new file mode 100644 index 0000000..5350329 --- /dev/null +++ b/web/src/features/accounts/account-new.tsx @@ -0,0 +1,260 @@ +// +// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { useCallback, useState } from "react"; +import { useForm, FormProvider } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslation } from "react-i18next"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useNavigate, Link } from "@tanstack/react-router"; +import { ArrowLeft, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Form } from "@/components/ui/form"; +import { useToast } from "@/hooks/use-toast"; +import { ToastAction } from "@/components/ui/toast"; +import { Breadcrumb } from "@/components/ui/breadcrumb"; +import { FixedHeader } from "@/components/layout/fixed-header"; +import { Main } from "@/components/layout/main"; +import { TabGeneral } from "./components/tab-general"; +import { TabServer } from "./components/tab-server"; +import { TabDownload } from "./components/tab-download"; +import { TabFilters } from "./components/tab-filters"; +import { create_account, autoconfig } from "@/api/account/api"; +import { getAccountSchema, type AccountFormValues } from "./components/schema"; +import type { AxiosError } from "axios"; + +const defaultValues: AccountFormValues = { + login_name: undefined, + account_name: undefined, + email: '', + imap: { + host: "", + port: 993, + encryption: 'Ssl', + auth: { auth_type: 'Password', password: undefined }, + use_proxy: undefined, + }, + enabled: true, + use_dangerous: false, + date_since: undefined, + date_before: undefined, + download_interval_min: 60, + download_batch_size: 30, + max_email_size_bytes: 100 * 1024 * 1024, + auto_download_new_mailboxes: true, + download_schedule: undefined, + archive_rules: undefined, +}; + +function SectionHeader({ title, description }: { title: string; description?: string }) { + return ( +
+

{title}

+ {description &&

{description}

} +
+ ); +} + +export function AccountNewPage() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [autoConfigLoading, setAutoConfigLoading] = useState(false); + + const accountSchema = getAccountSchema(false, t); + const form = useForm({ + mode: "onChange", + defaultValues, + resolver: zodResolver(accountSchema), + }); + + const createMutation = useMutation({ + mutationFn: create_account, + onSuccess: () => { + toast({ + title: t('accounts.accountCreated'), + description: t('accounts.accountCreatedDesc'), + action: {t('common.close')}, + }); + queryClient.invalidateQueries({ queryKey: ['account-list'] }); + navigate({ to: '/accounts' }); + }, + onError: (error: AxiosError) => { + const errorMessage = + (error.response?.data as { message?: string })?.message || + error.message || + t('accounts.creationFailed'); + toast({ + variant: "destructive", + title: t('accounts.accountCreationFailed'), + description: errorMessage as string, + action: {t('common.tryAgain')}, + }); + }, + }); + + const onSubmit = useCallback( + (data: AccountFormValues) => { + createMutation.mutate({ + email: data.email, + account_name: data.account_name, + login_name: data.login_name, + imap: { + ...data.imap, + auth: { + ...data.imap.auth, + password: data.imap.auth.auth_type === 'OAuth2' ? undefined : data.imap.auth.password, + }, + }, + enabled: data.enabled, + use_dangerous: data.use_dangerous, + date_since: data.date_since, + date_before: data.date_before, + download_interval_min: data.download_interval_min, + download_batch_size: data.download_batch_size, + max_email_size_bytes: data.max_email_size_bytes, + auto_download_new_mailboxes: data.auto_download_new_mailboxes, + download_schedule: data.download_schedule || null, + account_type: "IMAP", + archive_rules: data.archive_rules || null, + }); + }, + [createMutation] + ); + + const handleAutoConfig = async () => { + const email = form.getValues('email'); + if (!email) return; + const imap = form.getValues('imap'); + if (imap.host.trim() !== "" && imap.port > 0) return; + + setAutoConfigLoading(true); + try { + const result = await autoconfig(email); + if (result) { + form.setValue('imap.host', result.imap.host); + form.setValue('imap.port', result.imap.port); + form.setValue('imap.encryption', result.imap.encryption); + if (result.oauth2) form.setValue('imap.auth.auth_type', 'OAuth2'); + } + } catch (error) { + console.error('Auto-configuration failed:', error); + } + setAutoConfigLoading(false); + }; + + return ( + <> + +
+
+
+ + + {t('accounts.settings.backToAccounts')} + + +
+ +
+
+

{t('accounts.addAccount')}

+

{t('accounts.addNewEmailAccountHere')}

+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ + +
+
+ +
+
+
+
+ + ); +} diff --git a/web/src/features/accounts/account-settings-page.tsx b/web/src/features/accounts/account-settings-page.tsx new file mode 100644 index 0000000..8857fbe --- /dev/null +++ b/web/src/features/accounts/account-settings-page.tsx @@ -0,0 +1,279 @@ +// +// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { useCallback, useEffect } from "react"; +import { useForm, FormProvider } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslation } from "react-i18next"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; +import { ArrowLeft } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Form } from "@/components/ui/form"; +import { useToast } from "@/hooks/use-toast"; +import { ToastAction } from "@/components/ui/toast"; +import { Breadcrumb } from "@/components/ui/breadcrumb"; +import { FixedHeader } from "@/components/layout/fixed-header"; +import { Main } from "@/components/layout/main"; +import { TabGeneral } from "./components/tab-general"; +import { TabServer } from "./components/tab-server"; +import { TabDownload } from "./components/tab-download"; +import { TabFilters } from "./components/tab-filters"; +import { update_account, list_accounts, type AccountModel } from "@/api/account/api"; +import { getAccountSchema, type AccountFormValues } from "./components/schema"; +import type { AxiosError } from "axios"; +import { useQuery } from "@tanstack/react-query"; + +const emptyImap = { + host: "", + port: 0, + encryption: "None" as const, + auth: { auth_type: "Password" as const, password: undefined }, + use_proxy: undefined, +}; + +function mapAccountToFormValues(account: AccountModel): AccountFormValues { + const imap = { ...(account.imap ?? emptyImap) }; + imap.auth = { ...imap.auth, password: undefined }; + if ((imap as any).use_proxy === null) { + (imap as any).use_proxy = undefined; + } + + return { + account_name: account.account_name ?? undefined, + login_name: account.login_name ?? undefined, + email: account.email, + imap, + enabled: account.enabled, + use_dangerous: account.use_dangerous, + date_since: account.date_since ?? undefined, + date_before: account.date_before ?? undefined, + download_interval_min: account.download_interval_min ?? 60, + download_batch_size: account.download_batch_size ?? 30, + max_email_size_bytes: account.max_email_size_bytes ?? 100 * 1024 * 1024, + auto_download_new_mailboxes: account.auto_download_new_mailboxes ?? true, + download_schedule: account.download_schedule ?? undefined, + archive_rules: account.archive_rules ?? undefined, + }; +} + +function SectionHeader({ title, description }: { title: string; description?: string }) { + return ( +
+

{title}

+ {description &&

{description}

} +
+ ); +} + +interface AccountSettingsPageProps { + accountId: number; +} + +export function AccountSettingsPage({ accountId }: AccountSettingsPageProps) { + const { t } = useTranslation(); + //const navigate = useNavigate(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + const { data: accountList } = useQuery({ + queryKey: ['account-list'], + queryFn: list_accounts, + }); + + const account = accountList?.items?.find((a) => a.id === accountId); + + const accountSchema = getAccountSchema(true, t); + const form = useForm({ + mode: "onChange", + defaultValues: account ? mapAccountToFormValues(account) : undefined, + resolver: zodResolver(accountSchema), + }); + + useEffect(() => { + if (account) { + form.reset(mapAccountToFormValues(account)); + } + }, [account?.id]); + + const updateMutation = useMutation({ + mutationFn: (data: Record) => update_account(accountId, data), + onSuccess: () => { + toast({ + title: t('accounts.settings.saved'), + description: t('accounts.settings.savedDesc'), + action: {t('common.close')}, + }); + queryClient.invalidateQueries({ queryKey: ['account-list'] }); + }, + onError: (error: AxiosError) => { + const errorMessage = + (error.response?.data as { message?: string })?.message || + error.message || + t('accounts.updateFailed'); + toast({ + variant: "destructive", + title: t('accounts.accountUpdateFailed'), + description: errorMessage as string, + action: {t('common.tryAgain')}, + }); + }, + }); + + const onSubmit = useCallback( + (data: AccountFormValues) => { + const payload: Record = { + email: data.email, + account_name: data.account_name, + login_name: data.login_name, + imap: { + ...data.imap, + auth: { + ...data.imap.auth, + password: data.imap.auth.auth_type === 'OAuth2' + ? undefined + : (data.imap.auth.password ? data.imap.auth.password : undefined), + }, + }, + enabled: data.enabled, + use_dangerous: data.use_dangerous, + date_since: data.date_since, + date_before: data.date_before, + download_interval_min: data.download_interval_min, + download_batch_size: data.download_batch_size, + max_email_size_bytes: data.max_email_size_bytes, + auto_download_new_mailboxes: data.auto_download_new_mailboxes, + download_schedule: data.download_schedule || null, + archive_rules: data.archive_rules || null, + }; + + if (!data.date_since && !data.date_before) { + payload.clear_date_range = true; + } + if (!data.download_schedule && account?.download_schedule) { + payload.clear_download_schedule = true; + } + + updateMutation.mutate(payload); + }, + [updateMutation, account] + ); + + if (!account) { + return ( + <> + +
+
+ {t('accounts.settings.loading')} +
+
+ + ); + } + + return ( + <> + +
+
+
+ + + {t('accounts.settings.backToAccounts')} + + +
+ +
+
+

{account.email}

+

{t('accounts.updateTheEmailAccountHere')}

+
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ + +
+
+ +
+
+
+
+ + ); +} diff --git a/web/src/features/accounts/components/action-dialog.tsx b/web/src/features/accounts/components/action-dialog.tsx deleted file mode 100644 index 619a292..0000000 --- a/web/src/features/accounts/components/action-dialog.tsx +++ /dev/null @@ -1,368 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import { zodResolver } from '@hookform/resolvers/zod'; -import * as React from 'react'; -import { useForm } from 'react-hook-form'; -import { Button } from '@/components/ui/button'; -import { Form } from '@/components/ui/form'; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { useToast } from '@/hooks/use-toast'; -import Step1 from './step1'; -import Step2 from './step2'; -import Step3 from './step3'; -import Step4 from './step4'; -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"; -import { getAccountSchema, type AccountFormValues } from './schema'; - -export type Account = AccountFormValues; - -type Step = { - id: `step-${number}`; - name: string; - fields: (keyof Account)[]; -}; - -export type Steps = [...Step[]]; - -const getSteps = (t: (key: string) => string): Steps => [ - { id: "step-1", name: t('accounts.steps.emailAddress'), fields: ["email", "account_name"] }, - { id: "step-2", name: t('accounts.steps.imap'), fields: ["imap", "use_dangerous", "login_name"] }, - { id: "step-3", name: t('accounts.steps.syncPreferences'), fields: ["enabled", "date_since", "date_before", "download_interval_min", "download_batch_size", "max_email_size_bytes", "auto_download_new_mailboxes", "download_schedule"] }, - { id: "step-4", name: t('accounts.steps.summary'), fields: [] }, -]; - -const LAST_STEP = 4; - -interface Props { - currentRow?: AccountModel; - open: boolean; - onOpenChange: (open: boolean) => void; -} - -const defaultValues: Account = { - login_name: undefined, - account_name: undefined, - email: '', - imap: { - host: "", - port: 993, - encryption: 'Ssl', - auth: { - auth_type: 'Password', - password: undefined, - }, - use_proxy: undefined - }, - enabled: true, - use_dangerous: false, - date_since: undefined, - date_before: undefined, - download_interval_min: 60, - download_batch_size: 30, - max_email_size_bytes: 100 * 1024 * 1024, - auto_download_new_mailboxes: true, - download_schedule: undefined, -}; - -const emptyImap: ImapConfig = { - host: "", - port: 0, - encryption: "None", - auth: { auth_type: "Password", password: undefined }, - use_proxy: undefined, -}; - -const mapCurrentRowToFormValues = (currentRow: AccountModel): Account => { - const imap = { ...(currentRow.imap ?? emptyImap) }; - imap.auth = { ...imap.auth, password: undefined }; - if (imap.use_proxy === null) { - imap.use_proxy = undefined; - } - - return { - account_name: currentRow.account_name ?? undefined, - login_name: currentRow.login_name ?? undefined, - email: currentRow.email, - imap, - enabled: currentRow.enabled, - use_dangerous: currentRow.use_dangerous, - date_since: currentRow.date_since ?? undefined, - date_before: currentRow.date_before ?? undefined, - download_interval_min: currentRow.download_interval_min ?? 60, - download_batch_size: currentRow.download_batch_size ?? 30, - max_email_size_bytes: currentRow.max_email_size_bytes ?? 100 * 1024 * 1024, - auto_download_new_mailboxes: currentRow.auto_download_new_mailboxes ?? true, - download_schedule: currentRow.download_schedule ?? undefined, - }; -}; - -export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) { - const { t } = useTranslation(); - const steps = getSteps(t); - const isEdit = !!currentRow; - const [currentStep, setCurrentStep] = React.useState(1); - const { toast } = useToast(); - const [autoConfigLoading, setAutoConfigLoading] = React.useState(false); - - const accountSchema = getAccountSchema(isEdit, t); - const form = useForm({ - mode: "onChange", - defaultValues: isEdit ? mapCurrentRowToFormValues(currentRow) : defaultValues, - resolver: zodResolver(accountSchema), - }); - - const queryClient = useQueryClient(); - - const createMutation = useMutation({ - mutationFn: create_account, - onSuccess: handleSuccess, - onError: handleError, - }); - - const updateMutation = useMutation({ - mutationFn: (data: Record) => update_account(currentRow?.id!, data), - onSuccess: handleSuccess, - onError: handleError, - }); - - function handleSuccess() { - toast({ - title: isEdit ? t('accounts.accountUpdated') : t('accounts.accountCreated'), - description: isEdit ? t('accounts.accountUpdatedDesc') : t('accounts.accountCreatedDesc'), - action: {t('common.close')}, - }); - - queryClient.invalidateQueries({ queryKey: ['account-list'] }); - form.reset(); - onOpenChange(false); - } - - function handleError(error: AxiosError) { - const errorMessage = - (error.response?.data as { message?: string })?.message || - error.message || - (isEdit ? t('accounts.updateFailed') : t('accounts.creationFailed')); - - toast({ - variant: "destructive", - title: isEdit ? t('accounts.accountUpdateFailed') : t('accounts.accountCreationFailed'), - description: errorMessage as string, - action: {t('common.tryAgain')}, - }); - console.error(error); - } - - const onSubmit = React.useCallback( - (data: Account) => { - const commonData = { - email: data.email, - account_name: data.account_name, - login_name: data.login_name, - imap: { - ...data.imap, - auth: { - ...data.imap.auth, - password: data.imap.auth.auth_type === 'OAuth2' - ? undefined - : (isEdit && !data.imap.auth.password ? undefined : data.imap.auth.password), - }, - }, - enabled: data.enabled, - use_dangerous: data.use_dangerous, - date_since: data.date_since, - date_before: data.date_before, - download_interval_min: data.download_interval_min, - download_batch_size: data.download_batch_size, - max_email_size_bytes: data.max_email_size_bytes, - auto_download_new_mailboxes: data.auto_download_new_mailboxes, - download_schedule: data.download_schedule || null, - }; - if (isEdit) { - const isAllMode = !data.date_since && !data.date_before; - const clear_download_schedule = !data.download_schedule && currentRow?.download_schedule; - updateMutation.mutate({ - ...commonData, - ...(isAllMode ? { clear_date_range: true } : {}), - ...(clear_download_schedule ? { clear_download_schedule: true } : {}) - }); - } else { - createMutation.mutate({ ...commonData, account_type: "IMAP" }); - } - }, - [isEdit, updateMutation, createMutation] - ); - - const handleNav = async (index: number) => { - let isValid = true; - let failedStep = currentStep; - for (let i = currentStep - 1; i < index - 1 && isValid; i++) { - isValid = await form.trigger(steps[i].fields); - if (!isValid) failedStep = i; - } - if (isValid) setCurrentStep(index); - else setCurrentStep(failedStep); - }; - - async function handleContinue() { - const isValid = await form.trigger(steps[currentStep - 1].fields); - if (!isValid) return; - - if (currentStep === 1) { - let allValues = form.getValues(); - if (allValues.imap.host.trim() !== "" && allValues.imap.port > 0) { - handleNav(currentStep + 1); - return; - } - setAutoConfigLoading(true); - const email = form.getValues('email'); - form.setValue('login_name', email); - try { - const result = await autoconfig(email); - if (result) { - form.setValue('imap.host', result.imap.host); - form.setValue('imap.port', result.imap.port); - form.setValue('imap.encryption', result.imap.encryption); - if (result.oauth2) form.setValue('imap.auth.auth_type', 'OAuth2'); - } - } catch (error) { - console.error('Auto-configuration failed:', error); - } - setAutoConfigLoading(false); - handleNav(currentStep + 1); - } else { - handleNav(currentStep + 1); - } - } - - return ( - { - 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) => ( -
- - {step.name} -
- ))} -
- -
- {steps.map((step, index) => ( -
- -
- {t('accounts.step', { index: index + 1 })} - - {step.name} - -
-
- ))} -
- -
- -
-
- - {currentStep === 1 && } - {currentStep === 2 && } - {currentStep === 3 && } - {currentStep === 4 && } - - -
-
-
-
- - - {currentStep > 1 && ( - - )} - {currentStep < LAST_STEP && ( - - )} - {currentStep === LAST_STEP && ( - - )} - -
-
- ); -} diff --git a/web/src/features/accounts/components/add-account-dialog.tsx b/web/src/features/accounts/components/add-account-dialog.tsx deleted file mode 100644 index a26ccb0..0000000 --- a/web/src/features/accounts/components/add-account-dialog.tsx +++ /dev/null @@ -1,159 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// - -import * as React from 'react' -import { Mail, Database } from 'lucide-react' -import { useTranslation } from 'react-i18next' - -import { cn } from '@/lib/utils' -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { Label } from '@/components/ui/label' -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' -import { useAccountContext } from '../context' - -export type AddAccountType = 'IMAP' | 'NoSync' - -interface Props { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function AddAccountDialog({ - open, - onOpenChange -}: Props) { - const { t } = useTranslation() - const { setOpen } = useAccountContext() - const [value, setValue] = React.useState('IMAP') - - function handleContinue() { - if (value === 'IMAP') { - setOpen('add-imap') - } else { - setOpen('add-nosync') - } - } - - return ( - - - - - {t('accounts.add')} - - - - {t('accounts.selectAccountType')} - - - - setValue(v as AddAccountType)} - className="space-y-4 py-2" - > - - - - - - - - - - - - - ) -} \ No newline at end of file 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 6a7ff92..039097e 100644 --- a/web/src/features/accounts/components/data-table-row-actions.tsx +++ b/web/src/features/accounts/components/data-table-row-actions.tsx @@ -30,11 +30,12 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useAccountContext } from '../context' -import { Mailbox, MessageSquareMore } from 'lucide-react' +import { Mailbox, MessageSquareMore, Settings } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useCurrentUser } from '@/hooks/use-current-user' import { AccountModel, cancel_account_download, start_account_download } from '@/api/account/api' import { toast } from '@/hooks/use-toast' +import { useNavigate } from '@tanstack/react-router' interface DataTableRowActionsProps { row: Row @@ -43,6 +44,7 @@ interface DataTableRowActionsProps { export function DataTableRowActions({ row }: DataTableRowActionsProps) { const { t } = useTranslation() const { setOpen, setCurrentRow } = useAccountContext() + const navigate = useNavigate() const account_type = row.original.account_type; const { require_any_permission } = useCurrentUser() @@ -103,11 +105,10 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { {hasPermission && { - setCurrentRow(row.original) if (account_type === "IMAP") { - setOpen("edit-imap"); - } - if (account_type === "NoSync") { + navigate({ to: '/accounts/$id/settings', params: { id: String(row.original.id) } }); + } else { + setCurrentRow(row.original) setOpen("edit-nosync"); } }} @@ -117,6 +118,16 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { } + {account_type === "IMAP" && hasPermission && { + navigate({ to: '/accounts/$id/settings', params: { id: String(row.original.id) } }); + }} + > + {t('accounts.settings.settings')} + + + + } {account_type === "IMAP" && hasPermission && { setCurrentRow(row.original) diff --git a/web/src/features/accounts/components/pattern-input.tsx b/web/src/features/accounts/components/pattern-input.tsx new file mode 100644 index 0000000..48aa05e --- /dev/null +++ b/web/src/features/accounts/components/pattern-input.tsx @@ -0,0 +1,90 @@ +// +// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { X } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import type { MatchType, PatternEntry } from "@/lib/pattern-utils"; +import { isValidRegex } from "@/lib/pattern-utils"; + +interface PatternInputProps { + entry: PatternEntry; + onChange: (id: string, entry: Partial) => void; + onRemove: (id: string) => void; +} + +export function PatternInput({ entry, onChange, onRemove }: PatternInputProps) { + const { t } = useTranslation(); + + const matchTypeLabels: Record = { + contains: t("accounts.filters.matchType.contains"), + starts_with: t("accounts.filters.matchType.startsWith"), + ends_with: t("accounts.filters.matchType.endsWith"), + is_exactly: t("accounts.filters.matchType.isExactly"), + regex: t("accounts.filters.matchType.regex"), + }; + + const regexValid = entry.matchType === 'regex' ? isValidRegex(entry.value) : true; + const showRegexHint = entry.matchType === 'regex' && entry.value && !regexValid; + + return ( +
+ +
+ onChange(entry.id, { value: e.target.value })} + /> + {showRegexHint && ( + + Invalid regex + + )} +
+ +
+ ); +} diff --git a/web/src/features/accounts/components/schema.ts b/web/src/features/accounts/components/schema.ts index 80dc935..35b504f 100644 --- a/web/src/features/accounts/components/schema.ts +++ b/web/src/features/accounts/components/schema.ts @@ -67,6 +67,19 @@ const dateSelectionSchema = (t: (key: string) => string) => }) .optional() +const filterRuleSchema = z.object({ + include: z.array(z.string()), + exclude: z.array(z.string()), +}) + +const archiveRulesSchema = z.object({ + enabled: z.boolean(), + senders: filterRuleSchema, + subjects: filterRuleSchema, + skip_larger_than: z.number().int().positive().optional(), + spam_headers: z.array(z.string()), +}) + export const getAccountSchema = (isEdit: boolean, t: (key: string) => string) => z.object({ account_name: z.string().optional(), @@ -120,6 +133,7 @@ export const getAccountSchema = (isEdit: boolean, t: (key: string) => string) => }, { message: t('validation.invalidCronExpression') } ), + archive_rules: archiveRulesSchema.optional(), }) export type AccountFormValues = z.infer< diff --git a/web/src/features/accounts/components/step1.tsx b/web/src/features/accounts/components/step1.tsx deleted file mode 100644 index 949e3f0..0000000 --- a/web/src/features/accounts/components/step1.tsx +++ /dev/null @@ -1,88 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import { useFormContext } from "react-hook-form"; -import { - FormField, - FormItem, - FormLabel, - FormMessage, - FormControl, - FormDescription, -} from "@/components/ui/form"; - -import { Input } from "@/components/ui/input"; -import { Account } from "./action-dialog"; -import { useTranslation } from "react-i18next"; - -interface StepProps { - isEdit: boolean; -} - -export default function Step1({ isEdit }: StepProps) { - const { t } = useTranslation() - const { control } = useFormContext(); - - return ( - <> -

{t('accounts.emailAccountRegistration')}

-

- {t('accounts.emailAccountRegistrationDesc')} -

-
- ( - - - {t('accounts.emailAddress')}: - - - - - - {isEdit && ( - - {t('accounts.emailCannotBeModified')} - - )} - - )} - /> - ( - - - {t('accounts.name')}: - - - - - {t('accounts.optional')} - - - )} - /> -
- - ); -} \ No newline at end of file diff --git a/web/src/features/accounts/components/step2.tsx b/web/src/features/accounts/components/step2.tsx deleted file mode 100644 index edc9242..0000000 --- a/web/src/features/accounts/components/step2.tsx +++ /dev/null @@ -1,236 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import { - FormField, - FormItem, - FormLabel, - FormMessage, - FormControl, - FormDescription, -} from "@/components/ui/form"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Input } from "@/components/ui/input"; -import { useFormContext, useWatch } from "react-hook-form"; -import { Account } from "./action-dialog"; -import { PasswordInput } from "@/components/password-input"; -import useProxyList from "@/hooks/use-proxy"; -import { useTranslation } from "react-i18next"; -import { Checkbox } from "@/components/ui/checkbox"; - -interface StepProps { - isEdit: boolean; -} - -export default function Step2({ isEdit }: StepProps) { - const { t } = useTranslation() - const { control } = useFormContext(); - const { proxyOptions } = useProxyList(); - - const imapAuthMethod = useWatch({ - control, - name: "imap.auth.auth_type", - }); - - return ( - <> -
- ( - - - {t('accounts.imapHost')}: - - - - - - - )} - /> - ( - - - {t('accounts.imapPort')}: - - - field.onChange(parseInt(e.target.value, 10))} /> - - - - )} - /> - ( - - {t('accounts.imapEncryption')}: - - - {t('accounts.chooseEncryptionMethod')} - - - - )} - /> - ( - - {t('accounts.useDangerous')}: - - - - {t('accounts.useDangerousDescription')} - - )} - /> - ( - - - {t('accounts.login_name')}: - - - - - {t('accounts.nameDescription')} - - - )} - /> - ( - - {t('accounts.imapAuthMethod')}: - - - {t('accounts.chooseAuthMethod')} - - - - )} - /> - {imapAuthMethod === "Password" && ( - ( - - - {t('accounts.imapPassword')}: - - - - - - {isEdit && ( - - {t('accounts.leaveEmptyToKeepExisting')} - - )} - - )} - /> - )} - ( - - {t('accounts.useProxy')} ({t('accounts.optional')}): - - - - - {t('accounts.imapProxy')} - - - - )} - /> -
- - ); -} \ No newline at end of file diff --git a/web/src/features/accounts/components/step3.tsx b/web/src/features/accounts/components/step3.tsx deleted file mode 100644 index 558ce8a..0000000 --- a/web/src/features/accounts/components/step3.tsx +++ /dev/null @@ -1,567 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import { - FormField, - FormItem, - FormLabel, - FormMessage, - FormControl, - FormDescription, -} from "@/components/ui/form"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Input } from "@/components/ui/input"; -import { useFormContext } from "react-hook-form"; -import { Account } from "./action-dialog"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { Button } from "@/components/ui/button"; -import { format } from "date-fns"; -import { CalendarIcon } from "lucide-react"; -import { Calendar } from "@/components/ui/calendar"; -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'; -type ScheduleMode = 'interval' | 'cron'; -type CronMode = 'simple' | 'advanced'; -type CronFrequency = 'daily' | 'weekly' | 'monthly'; - -interface CronSimpleState { - frequency: CronFrequency; - hour: number; - minute: number; - dayOfWeek: number; - dayOfMonth: number; -} - -const DEFAULT_CRON_SIMPLE: CronSimpleState = { - frequency: 'daily', - hour: 0, - minute: 0, - dayOfWeek: 1, - dayOfMonth: 1, -}; - -function buildCronFromSimple(s: CronSimpleState): string { - switch (s.frequency) { - case 'daily': - return `0 ${s.minute} ${s.hour} * * *`; - case 'weekly': - return `0 ${s.minute} ${s.hour} * * ${s.dayOfWeek}`; - case 'monthly': - return `0 ${s.minute} ${s.hour} ${s.dayOfMonth} * *`; - } -} - -function tryParseCronToSimple(cron: string): CronSimpleState | null { - const fields = cron.trim().split(/\s+/); - if (fields.length < 6) return null; - - const sec = fields[0]; - const min = fields[1]; - const hour = fields[2]; - const dom = fields[3]; - const month = fields[4]; - const dow = fields[5]; - - if (sec !== '0') return null; - if (month !== '*') return null; - - const minuteVal = parseInt(min, 10); - const hourVal = parseInt(hour, 10); - if (isNaN(minuteVal) || isNaN(hourVal)) return null; - - if (dom === '*' && dow === '*') { - return { frequency: 'daily', hour: hourVal, minute: minuteVal, dayOfWeek: 1, dayOfMonth: 1 }; - } - if (dom === '*') { - const dowVal = parseInt(dow, 10); - if (!isNaN(dowVal)) { - return { frequency: 'weekly', hour: hourVal, minute: minuteVal, dayOfWeek: dowVal, dayOfMonth: 1 }; - } - } - if (dow === '*') { - const domVal = parseInt(dom, 10); - if (!isNaN(domVal)) { - return { frequency: 'monthly', hour: hourVal, minute: minuteVal, dayOfWeek: 1, dayOfMonth: domVal }; - } - } - return null; -} - -export default function Step3() { - const { t } = useTranslation(); - const { control, getValues, setValue } = useFormContext(); - const current = getValues(); - - 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 [scheduleMode, setScheduleMode] = useState(() => { - if (current.download_schedule) return 'cron'; - return 'interval'; - }); - - const [cronMode, setCronMode] = useState(() => { - if (current.download_schedule && tryParseCronToSimple(current.download_schedule)) { - return 'simple'; - } - if (current.download_schedule) return 'advanced'; - return 'simple'; - }); - - const [cronSimple, setCronSimple] = useState(() => { - if (current.download_schedule) { - return tryParseCronToSimple(current.download_schedule) ?? DEFAULT_CRON_SIMPLE; - } - return DEFAULT_CRON_SIMPLE; - }); - - const updateCronFromSimple = (partial: Partial) => { - const next = { ...cronSimple, ...partial }; - setCronSimple(next); - setValue('download_schedule', buildCronFromSimple(next)); - }; - - 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' }); - } - }; - - const handleScheduleModeChange = (mode: ScheduleMode) => { - setScheduleMode(mode); - if (mode === 'interval') { - setValue("download_schedule", undefined); - } else { - setValue("download_interval_min", 60); - if (cronMode === 'simple') { - updateCronFromSimple(cronSimple); - } - } - }; - - return ( -
-
- - {t('accounts.scheduleMode')} - - {t('accounts.scheduleModeDescription')} - - - -
- {scheduleMode === 'interval' ? ( - ( - - {t('accounts.downloadInterval')} - - field.onChange(parseInt(e.target.value, 10))} /> - - - - {t('accounts.downloadIntervalPlaceholder')} - - - )} - /> - ) : ( -
-
- {/* {t('accounts.downloadSchedule')} */} -
- - -
-
- - {cronMode === 'simple' ? ( -
- - {t('accounts.cronFrequency')} - - - - - {t('accounts.cronHour')} - - - - : - - - {t('accounts.cronMinute')} - - - - {cronSimple.frequency === 'weekly' && ( - - {t('accounts.cronDayOfWeek')} - - - )} - - {cronSimple.frequency === 'monthly' && ( - - {t('accounts.cronDayOfMonth')} - - - )} - -
- = {buildCronFromSimple(cronSimple)} -
-
- ) : ( - ( - - - - - - {t('accounts.downloadScheduleDescription')} - - - - )} - /> - )} - - {cronMode === 'simple' && ( - {t('accounts.downloadScheduleDescription')} - )} - -
- {t('accounts.cronTimezoneNote')} -
-
- )} - ( - - {t('accounts.downloadBatchSize')} - - field.onChange(parseInt(e.target.value, 10))} /> - - - - {t('accounts.downloadBatchSizeDescription')} - - - )} - /> - { - const BYTES_PER_MB = 1024 * 1024; - - return ( - - {t('accounts.maxEmailSizeBytes')} - -
- { - const parsed = parseInt(e.target.value, 10); - field.onChange(isNaN(parsed) ? parsed : parsed * BYTES_PER_MB); - }} - /> - MB -
-
- - - {t('accounts.maxEmailSizeBytesDescription')} - -
- ); - }} - /> -
-
- - ( - - - - -
- {t('accounts.enabled')} - {t('accounts.enabledDescription')} -
-
- )} - /> - -
-
- - {t('accounts.downloadScope')} - - {t('accounts.downloadScopeDescription')} - - - -
- {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} - /> - - - - ; - - }} - /> - )} - - {(syncMode === 'since_relative' || syncMode === 'before_relative') && ( -
- ( - - {t('accounts.duration', 'Duration')} - - field.onChange(parseInt(e.target.value, 10))} /> - - - - )} - /> - ( - - {t('accounts.unit', 'Unit')} - - - - )} - /> -
- )} -
-
- -
- - ( - - - - -
- {t('accounts.autoDownloadNewMailboxes')} - {t('accounts.autoDownloadNewMailboxesDescription')} -
-
- )} - /> - -
-
- ); -} \ No newline at end of file diff --git a/web/src/features/accounts/components/step4.tsx b/web/src/features/accounts/components/step4.tsx deleted file mode 100644 index adcb128..0000000 --- a/web/src/features/accounts/components/step4.tsx +++ /dev/null @@ -1,188 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import { useFormContext } from "react-hook-form"; -import { Account } from "./action-dialog"; -import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion"; -import { useTranslation } from "react-i18next"; -import useProxyList from "@/hooks/use-proxy"; - -export default function Step4() { - const { t } = useTranslation(); - const { getValues } = useFormContext(); - const { getUrlById } = useProxyList(); - 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} - - - - {t('accounts.name')}: - {summaryData.account_name ?? t('accounts.notAvailable')} - - - - {t('accounts.login_name')}: - {summaryData.login_name ?? t('accounts.notAvailable')} - - - - {t('accounts.imap')}: - -
- - - - - - - - - - - - - - - - - - - - - - - {summaryData.imap.auth.auth_type === 'Password' && ( - - - - - )} - - - - - -
{t('accounts.host')}:{summaryData.imap.host}
{t('accounts.port')}:{summaryData.imap.port}
{t('accounts.encryption')}:{summaryData.imap.encryption}
{t('accounts.useDangerous')}:{`${summaryData.use_dangerous}`}
{t('accounts.authType')}:{summaryData.imap.auth.auth_type}
{t('accounts.password')}:{summaryData.imap.auth.password}
{t('accounts.useProxyField')}: - {(() => { - if (!summaryData.imap.use_proxy) { - return t('accounts.useNoProxy'); - } - const proxyUrl = getUrlById(summaryData.imap.use_proxy); - return proxyUrl || `${t('common.yes')} (${summaryData.imap.use_proxy})`; - })()} -
-
-
-
- - - - {t('accounts.downloadScope')}: - - - - {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.downloadAll')} - - )} -
-
- - - - {t('accounts.downloadInterval')}: - {summaryData.download_interval_min} {t('accounts.minutes')} - - - - {t('accounts.downloadBatchSize')}: - {summaryData.download_batch_size} - - - - {t('accounts.maxEmailSizeBytes')}: - {summaryData.max_email_size_bytes ? `${(summaryData.max_email_size_bytes / 1024 / 1024).toFixed(0)} MB` : t('accounts.maxEmailSizeBytesUnlimited')} - - - - {t('accounts.downloadSchedule')}: - {summaryData.download_schedule || t('accounts.notAvailable')} - - - - {t('accounts.autoDownloadNewMailboxes')}: - {summaryData.auto_download_new_mailboxes ? t('common.yes') : t('common.no')} - -
-
- ); -} diff --git a/web/src/features/accounts/components/tab-download.tsx b/web/src/features/accounts/components/tab-download.tsx new file mode 100644 index 0000000..eea2290 --- /dev/null +++ b/web/src/features/accounts/components/tab-download.tsx @@ -0,0 +1,564 @@ +// +// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { useState } from "react"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { format } from "date-fns"; +import { CalendarIcon } from "lucide-react"; +import { enUS } from "date-fns/locale"; +import { + FormField, + FormItem, + FormLabel, + FormMessage, + FormControl, + FormDescription, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Calendar } from "@/components/ui/calendar"; +import { cn, dateFnsLocaleMap } from "@/lib/utils"; +import i18n from "@/i18n"; +import { AccountFormValues } from "./schema"; + +type SyncMode = 'all' | 'since_fixed' | 'since_relative' | 'before_relative'; +type ScheduleMode = 'interval' | 'cron'; +type CronMode = 'simple' | 'advanced'; +type CronFrequency = 'daily' | 'weekly' | 'monthly'; + +interface CronSimpleState { + frequency: CronFrequency; + hour: number; + minute: number; + dayOfWeek: number; + dayOfMonth: number; +} + +const DEFAULT_CRON_SIMPLE: CronSimpleState = { + frequency: 'daily', + hour: 0, + minute: 0, + dayOfWeek: 1, + dayOfMonth: 1, +}; + +function buildCronFromSimple(s: CronSimpleState): string { + switch (s.frequency) { + case 'daily': + return `0 ${s.minute} ${s.hour} * * *`; + case 'weekly': + return `0 ${s.minute} ${s.hour} * * ${s.dayOfWeek}`; + case 'monthly': + return `0 ${s.minute} ${s.hour} ${s.dayOfMonth} * *`; + } +} + +function tryParseCronToSimple(cron: string): CronSimpleState | null { + const fields = cron.trim().split(/\s+/); + if (fields.length < 6) return null; + const sec = fields[0]; + const min = fields[1]; + const hour = fields[2]; + const dom = fields[3]; + const month = fields[4]; + const dow = fields[5]; + if (sec !== '0') return null; + if (month !== '*') return null; + const minuteVal = parseInt(min, 10); + const hourVal = parseInt(hour, 10); + if (isNaN(minuteVal) || isNaN(hourVal)) return null; + if (dom === '*' && dow === '*') { + return { frequency: 'daily', hour: hourVal, minute: minuteVal, dayOfWeek: 1, dayOfMonth: 1 }; + } + if (dom === '*') { + const dowVal = parseInt(dow, 10); + if (!isNaN(dowVal)) { + return { frequency: 'weekly', hour: hourVal, minute: minuteVal, dayOfWeek: dowVal, dayOfMonth: 1 }; + } + } + if (dow === '*') { + const domVal = parseInt(dom, 10); + if (!isNaN(domVal)) { + return { frequency: 'monthly', hour: hourVal, minute: minuteVal, dayOfWeek: 1, dayOfMonth: domVal }; + } + } + return null; +} + +export function TabDownload() { + const { t } = useTranslation(); + const { control, getValues, setValue } = useFormContext(); + const current = getValues(); + + 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 [scheduleMode, setScheduleMode] = useState(() => { + if (current.download_schedule) return 'cron'; + return 'interval'; + }); + + const [cronMode, setCronMode] = useState(() => { + if (current.download_schedule && tryParseCronToSimple(current.download_schedule)) { + return 'simple'; + } + if (current.download_schedule) return 'advanced'; + return 'simple'; + }); + + const [cronSimple, setCronSimple] = useState(() => { + if (current.download_schedule) { + return tryParseCronToSimple(current.download_schedule) ?? DEFAULT_CRON_SIMPLE; + } + return DEFAULT_CRON_SIMPLE; + }); + + const updateCronFromSimple = (partial: Partial) => { + const next = { ...cronSimple, ...partial }; + setCronSimple(next); + setValue('download_schedule', buildCronFromSimple(next)); + }; + + 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' }); + } + }; + + const handleScheduleModeChange = (mode: ScheduleMode) => { + setScheduleMode(mode); + if (mode === 'interval') { + setValue("download_schedule", undefined); + } else { + setValue("download_interval_min", 60); + if (cronMode === 'simple') { + updateCronFromSimple(cronSimple); + } + } + }; + + const BYTES_PER_MB = 1024 * 1024; + + return ( +
+ {/* Schedule */} +
+

+ {t('accounts.settings.schedule')} +

+ + + {t('accounts.scheduleMode')} + {t('accounts.scheduleModeDescription')} + + + +
+ {scheduleMode === 'interval' && ( + ( + + {t('accounts.downloadInterval')}* + + field.onChange(parseInt(e.target.value, 10))} /> + + {t('accounts.downloadIntervalPlaceholder')} + + + )} + /> + )} +
+ + {scheduleMode === 'cron' && ( +
+
+
+ + +
+
+ + {cronMode === 'simple' ? ( + <> +
+ + {t('accounts.cronFrequency')} + + + + + {t('accounts.cronHour')} + + + + : + + + {t('accounts.cronMinute')} + + + + {cronSimple.frequency === 'weekly' && ( + + {t('accounts.cronDayOfWeek')} + + + )} + + {cronSimple.frequency === 'monthly' && ( + + {t('accounts.cronDayOfMonth')} + + + )} +
+ +

+ {cronSimple.frequency === 'daily' && t('accounts.cronSummaryDaily', { hour: String(cronSimple.hour).padStart(2, '0'), minute: String(cronSimple.minute).padStart(2, '0') })} + {cronSimple.frequency === 'weekly' && (() => { + const dayNames = ['cronSunday', 'cronMonday', 'cronTuesday', 'cronWednesday', 'cronThursday', 'cronFriday', 'cronSaturday']; + return t('accounts.cronSummaryWeekly', { hour: String(cronSimple.hour).padStart(2, '0'), minute: String(cronSimple.minute).padStart(2, '0'), day: t(`accounts.${dayNames[cronSimple.dayOfWeek]}`) }); + })()} + {cronSimple.frequency === 'monthly' && t('accounts.cronSummaryMonthly', { hour: String(cronSimple.hour).padStart(2, '0'), minute: String(cronSimple.minute).padStart(2, '0'), day: cronSimple.dayOfMonth })} + {cronSimple.frequency === 'monthly' && cronSimple.dayOfMonth > 28 && ( + {t('accounts.cronMonthAlignNote')} + )} +

+ + ) : ( + ( + + + + + {t('accounts.downloadScheduleDescription')} + + + )} + /> + )} + +
+ {t('accounts.cronTimezoneNote')} +
+
+ )} +
+ +
+ + {/* Batch & Size */} +
+

+ {t('accounts.settings.performance')} +

+
+ ( + + {t('accounts.downloadBatchSize')}* + + field.onChange(parseInt(e.target.value, 10))} /> + + {t('accounts.downloadBatchSizeDescription')} + + + )} + /> + ( + + {t('accounts.maxEmailSizeBytes')}* + +
+ { + const parsed = parseInt(e.target.value, 10); + field.onChange(isNaN(parsed) ? parsed : parsed * BYTES_PER_MB); + }} + /> + MB +
+
+ {t('accounts.maxEmailSizeBytesDescription')} + +
+ )} + /> +
+
+ +
+ + {/* Download Scope */} +
+

+ {t('accounts.settings.scope')} +

+ + + {t('accounts.downloadScope')} + {t('accounts.downloadScopeDescription')} + + + {syncMode === 'all' && t('accounts.downloadAllDesc')} + {syncMode === 'since_fixed' && t('accounts.sinceFixedDesc')} + {syncMode === 'since_relative' && t('accounts.sinceRelativeDesc')} + {syncMode === 'before_relative' && t('accounts.beforeRelativeDesc')} + + + +
+ {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} + /> + + + + + ); + }} + /> + )} + + {(syncMode === 'since_relative' || syncMode === 'before_relative') && ( +
+ ( + + {t('accounts.duration')}* + + field.onChange(parseInt(e.target.value, 10))} /> + + + + )} + /> + ( + + {t('accounts.unit', 'Unit')}* + + + + )} + /> +
+ )} +
+
+ +
+ + ( + + + + +
+ {t('accounts.autoDownloadNewMailboxes')} + {t('accounts.autoDownloadNewMailboxesDescription')} +
+
+ )} + /> +
+ ); +} diff --git a/web/src/features/accounts/components/tab-filters.tsx b/web/src/features/accounts/components/tab-filters.tsx new file mode 100644 index 0000000..7d75673 --- /dev/null +++ b/web/src/features/accounts/components/tab-filters.tsx @@ -0,0 +1,514 @@ +// +// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { useFormContext, useWatch } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { + FormField, + FormItem, + FormLabel, + FormMessage, + FormControl, + FormDescription, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Button } from "@/components/ui/button"; +import { Plus, HelpCircle } from "lucide-react"; +import { PatternInput } from "./pattern-input"; +import type { PatternEntry } from "@/lib/pattern-utils"; +import { newPatternId, simplePatternToRegex } from "@/lib/pattern-utils"; +import { useState } from "react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { AccountFormValues } from "./schema"; + +const SUGGESTED_SPAM_HEADERS = [ + 'X-Spam-Flag', + 'X-Spam', + 'X-Spam-Status', + 'X-Barracuda-Spam-Status', + 'X-Barracuda-Spam-Flag', + 'X-MS-Exchange-Organization-SCL', +]; + +function toPatternEntries(patterns: string[]): PatternEntry[] { + return patterns.map((p) => ({ + id: newPatternId(), + matchType: 'regex' as const, + value: p, + })); +} + +function patternsToRegexList(entries: PatternEntry[]): string[] { + return entries + .filter((e) => e.value.trim() !== '') + .map((e) => simplePatternToRegex(e.matchType, e.value)); +} + +export function TabFilters() { + const { t } = useTranslation(); + const { control, setValue } = useFormContext(); + const archiveRules = useWatch({ control, name: 'archive_rules' }); + + const enabled = archiveRules?.enabled ?? false; + const sendersInclude = archiveRules?.senders?.include ?? []; + const sendersExclude = archiveRules?.senders?.exclude ?? []; + const subjectsInclude = archiveRules?.subjects?.include ?? []; + const subjectsExclude = archiveRules?.subjects?.exclude ?? []; + const spamHeaders = archiveRules?.spam_headers ?? []; + + const [senderIncludeEntries, setSenderIncludeEntries] = useState( + () => toPatternEntries(sendersInclude) + ); + const [senderExcludeEntries, setSenderExcludeEntries] = useState( + () => toPatternEntries(sendersExclude) + ); + const [subjectIncludeEntries, setSubjectIncludeEntries] = useState( + () => toPatternEntries(subjectsInclude) + ); + const [subjectExcludeEntries, setSubjectExcludeEntries] = useState( + () => toPatternEntries(subjectsExclude) + ); + + // Resync local state when form values change externally (e.g. after form.reset) + const [lastSyncKey, setLastSyncKey] = useState(''); + const syncKey = JSON.stringify({ sendersInclude, sendersExclude, subjectsInclude, subjectsExclude }); + if (syncKey !== lastSyncKey) { + setLastSyncKey(syncKey); + setSenderIncludeEntries(toPatternEntries(sendersInclude)); + setSenderExcludeEntries(toPatternEntries(sendersExclude)); + setSubjectIncludeEntries(toPatternEntries(subjectsInclude)); + setSubjectExcludeEntries(toPatternEntries(subjectsExclude)); + } + + const syncToForm = ( + includeEntries: PatternEntry[], + excludeEntries: PatternEntry[], + fieldPrefix: string + ) => { + const include = patternsToRegexList(includeEntries); + const exclude = patternsToRegexList(excludeEntries); + setValue(`${fieldPrefix}.include` as any, include); + setValue(`${fieldPrefix}.exclude` as any, exclude); + }; + + const addEntry = ( + side: 'include' | 'exclude', + includeEntries: PatternEntry[], + excludeEntries: PatternEntry[], + setIncludeEntries: React.Dispatch>, + setExcludeEntries: React.Dispatch>, + fieldPrefix: string + ) => { + const newEntry: PatternEntry = { id: newPatternId(), matchType: 'contains', value: '' }; + const newInclude = side === 'include' ? [...includeEntries, newEntry] : includeEntries; + const newExclude = side === 'exclude' ? [...excludeEntries, newEntry] : excludeEntries; + setIncludeEntries(newInclude); + setExcludeEntries(newExclude); + syncToForm(newInclude, newExclude, fieldPrefix); + }; + + const updateEntry = ( + id: string, + partial: Partial, + side: 'include' | 'exclude', + includeEntries: PatternEntry[], + excludeEntries: PatternEntry[], + setIncludeEntries: React.Dispatch>, + setExcludeEntries: React.Dispatch>, + fieldPrefix: string + ) => { + if (side === 'include') { + const updated = includeEntries.map((e) => (e.id === id ? { ...e, ...partial } : e)); + setIncludeEntries(updated); + syncToForm(updated, excludeEntries, fieldPrefix); + } else { + const updated = excludeEntries.map((e) => (e.id === id ? { ...e, ...partial } : e)); + setExcludeEntries(updated); + syncToForm(includeEntries, updated, fieldPrefix); + } + }; + + const removeEntry = ( + id: string, + side: 'include' | 'exclude', + includeEntries: PatternEntry[], + excludeEntries: PatternEntry[], + setIncludeEntries: React.Dispatch>, + setExcludeEntries: React.Dispatch>, + fieldPrefix: string + ) => { + if (side === 'include') { + const filtered = includeEntries.filter((e) => e.id !== id); + setIncludeEntries(filtered); + syncToForm(filtered, excludeEntries, fieldPrefix); + } else { + const filtered = excludeEntries.filter((e) => e.id !== id); + setExcludeEntries(filtered); + syncToForm(includeEntries, filtered, fieldPrefix); + } + }; + + const handleEnableChange = (checked: boolean) => { + if (checked) { + setValue('archive_rules', { + enabled: true, + senders: { include: [], exclude: [] }, + subjects: { include: [], exclude: [] }, + skip_larger_than: undefined, + spam_headers: [], + }); + } else { + setValue('archive_rules', undefined); + } + }; + + const addSpamHeader = (header: string) => { + if (!spamHeaders.includes(header)) { + setValue('archive_rules.spam_headers', [...spamHeaders, header]); + } + }; + + const removeSpamHeader = (header: string) => { + setValue('archive_rules.spam_headers', spamHeaders.filter((h) => h !== header)); + }; + + const [newSpamHeader, setNewSpamHeader] = useState(''); + + const handleAddCustomSpamHeader = () => { + const trimmed = newSpamHeader.trim(); + if (trimmed && !spamHeaders.includes(trimmed)) { + setValue('archive_rules.spam_headers', [...spamHeaders, trimmed]); + setNewSpamHeader(''); + } + }; + + const BYTES_PER_MB = 1024 * 1024; + + return ( +
+ {/* Master Switch */} +
+ + + + +
+ {t('accounts.filters.enableFiltering')} + + {t('accounts.filters.enableFilteringDesc')} + +
+
+
+ + {enabled && ( + <> + {/* Sender Filters */} +
+
+

{t('accounts.filters.senderFilter')}

+ + + + + + {t('accounts.filters.senderFilterHelp')} + + +
+ +
+
+

+ {t('accounts.filters.include')} +

+ {senderIncludeEntries.length === 0 ? ( +

+ {t('accounts.filters.noIncludePatterns')} +

+ ) : ( +
+ {senderIncludeEntries.map((entry) => ( + + updateEntry(id, partial, 'include', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders') + } + onRemove={(id) => + removeEntry(id, 'include', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders') + } + /> + ))} +
+ )} + +
+ +
+

+ {t('accounts.filters.exclude')} +

+ {senderExcludeEntries.length === 0 ? ( +

+ {t('accounts.filters.noExcludePatterns')} +

+ ) : ( +
+ {senderExcludeEntries.map((entry) => ( + + updateEntry(id, partial, 'exclude', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders') + } + onRemove={(id) => + removeEntry(id, 'exclude', senderIncludeEntries, senderExcludeEntries, setSenderIncludeEntries, setSenderExcludeEntries, 'archive_rules.senders') + } + /> + ))} +
+ )} + +
+
+
+ + {/* Subject Filters */} +
+
+

{t('accounts.filters.subjectFilter')}

+ + + + + + {t('accounts.filters.subjectFilterHelp')} + + +
+
+
+

+ {t('accounts.filters.include')} +

+ {subjectIncludeEntries.length === 0 ? ( +

+ {t('accounts.filters.noIncludePatterns')} +

+ ) : ( +
+ {subjectIncludeEntries.map((entry) => ( + + updateEntry(id, partial, 'include', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects') + } + onRemove={(id) => + removeEntry(id, 'include', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects') + } + /> + ))} +
+ )} + +
+ +
+

+ {t('accounts.filters.exclude')} +

+ {subjectExcludeEntries.length === 0 ? ( +

+ {t('accounts.filters.noExcludePatterns')} +

+ ) : ( +
+ {subjectExcludeEntries.map((entry) => ( + + updateEntry(id, partial, 'exclude', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects') + } + onRemove={(id) => + removeEntry(id, 'exclude', subjectIncludeEntries, subjectExcludeEntries, setSubjectIncludeEntries, setSubjectExcludeEntries, 'archive_rules.subjects') + } + /> + ))} +
+ )} + +
+
+
+ + {/* Size Limit */} +
+
+

{t('accounts.filters.sizeLimit')}

+
+ ( + + {t('accounts.filters.skipLargerThan')} + +
+ { + const parsed = parseInt(e.target.value, 10); + field.onChange(isNaN(parsed) ? undefined : parsed * BYTES_PER_MB); + }} + /> + MB +
+
+ {t('accounts.filters.sizeLimitDesc')} + +
+ )} + /> +
+ + {/* Spam Headers */} +
+
+

{t('accounts.filters.spamHeaders')}

+ + + + + + {t('accounts.filters.spamHeadersHelp')} + + +
+
+ {spamHeaders.length > 0 ? ( + spamHeaders.map((header) => ( +
+
+ {header} +
+ +
+ )) + ) : ( +

+ {t('accounts.filters.noSpamHeaders')} +

+ )} +
+ +
+ setNewSpamHeader(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddCustomSpamHeader(); + } + }} + /> + +
+ +
+

{t('accounts.filters.suggestions')}

+
+ {SUGGESTED_SPAM_HEADERS.filter((h) => !spamHeaders.includes(h)).map((header) => ( + + ))} +
+
+
+ + )} +
+ ); +} diff --git a/web/src/features/accounts/components/tab-general.tsx b/web/src/features/accounts/components/tab-general.tsx new file mode 100644 index 0000000..2741d0d --- /dev/null +++ b/web/src/features/accounts/components/tab-general.tsx @@ -0,0 +1,91 @@ +// +// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { + FormField, + FormItem, + FormLabel, + FormMessage, + FormControl, + FormDescription, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { AccountFormValues } from "./schema"; + +interface TabGeneralProps { + isEdit?: boolean; +} + +export function TabGeneral({ isEdit }: TabGeneralProps) { + const { t } = useTranslation(); + const { control } = useFormContext(); + + return ( +
+ ( + + {t('accounts.email')}* + + + + {isEdit && ( + {t('accounts.emailCannotBeModified')} + )} + + + )} + /> + + ( + + {t('accounts.name')} + + + + + + )} + /> + + ( + + + + +
+ {t('accounts.enabled')} + {t('accounts.enabledDescription')} +
+
+ )} + /> +
+ ); +} diff --git a/web/src/features/accounts/components/tab-server.tsx b/web/src/features/accounts/components/tab-server.tsx new file mode 100644 index 0000000..44f9121 --- /dev/null +++ b/web/src/features/accounts/components/tab-server.tsx @@ -0,0 +1,180 @@ +// +// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { + FormField, + FormItem, + FormLabel, + FormMessage, + FormControl, + FormDescription, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { PasswordInput } from "@/components/password-input"; +import { AccountFormValues } from "./schema"; + +interface TabServerProps { + isEdit?: boolean; +} + +export function TabServer({ isEdit }: TabServerProps) { + const { t } = useTranslation(); + const { control, watch } = useFormContext(); + const authType = watch('imap.auth.auth_type'); + + return ( +
+ ( + + {t('accounts.imapHost')}* + + + + + + )} + /> + +
+ ( + + {t('accounts.imapPort')}* + + field.onChange(parseInt(e.target.value, 10))} + placeholder={t('accounts.imapPortPlaceholder')} + /> + + + + )} + /> + + ( + + {t('accounts.imapEncryption')}* + + + + )} + /> +
+ + ( + + {t('accounts.login_name')} + + + + {t('accounts.nameDescription')} + + + )} + /> + + ( + + {t('accounts.imapAuthMethod')}* + + + + )} + /> + + {authType === 'Password' && ( + ( + + {t('accounts.imapPassword')}{!isEdit && *} + + + + {isEdit && {t('accounts.leaveEmptyToKeepPassword')}} + + + )} + /> + )} + + ( + + + + +
+ {t('accounts.useDangerous')} +
+
+ )} + /> +
+ ); +} diff --git a/web/src/features/accounts/context/index.tsx b/web/src/features/accounts/context/index.tsx index b0b93fd..e381d09 100644 --- a/web/src/features/accounts/context/index.tsx +++ b/web/src/features/accounts/context/index.tsx @@ -21,10 +21,7 @@ import { AccountModel } from '@/api/account/api'; import React from 'react' export type AccountDialogType = - | 'add' - | 'add-imap' | 'add-nosync' - | 'edit-imap' | 'edit-nosync' | 'delete' | 'detail' diff --git a/web/src/features/accounts/index.tsx b/web/src/features/accounts/index.tsx index d5f34e1..d80c51a 100644 --- a/web/src/features/accounts/index.tsx +++ b/web/src/features/accounts/index.tsx @@ -19,16 +19,16 @@ import { useState } from 'react' import useDialogState from '@/hooks/use-dialog-state' +import { useNavigate } from '@tanstack/react-router' import { Button } from '@/components/ui/button' import { Main } from '@/components/layout/main' -import { AccountActionDialog } from './components/action-dialog' import { useColumns } from './components/columns' import { AccountDeleteDialog } from './components/delete-dialog' import { AccountTable } from './components/table' import AccountProvider, { type AccountDialogType, } from './context' -import { Plus } from 'lucide-react' +import { Mail, Database } from 'lucide-react' import Logo from '@/assets/logo.svg' import { AccountDetailDrawer } from './components/account-detail' import { AccountModel, list_accounts } from '@/api/account/api' @@ -42,10 +42,10 @@ import { NoSyncAccountDialog } from './components/nosync-dialog' import { useTranslation } from 'react-i18next' import { AccountAccessAssignmentDialog } from './components/access-assignment-dialog' import { useCurrentUser } from '@/hooks/use-current-user' -import { AddAccountDialog } from './components/add-account-dialog' export default function Accounts() { const { t } = useTranslation() + const navigate = useNavigate() const columns = useColumns() // Dialog states const [currentRow, setCurrentRow] = useState(null) @@ -77,15 +77,14 @@ export default function Accounts() {

{require_any_permission(['system:root', 'account:create']) &&
-
- -
+ +
} @@ -107,8 +106,13 @@ export default function Accounts() { {t('accounts.noAccountConfigurationsDesc')}

- +
@@ -117,16 +121,6 @@ export default function Accounts() { - setOpen('add')} /> - - setOpen('add-imap')} - /> - { - setOpen('edit-imap') - setTimeout(() => { - setCurrentRow(null) - }, 500) - }} - currentRow={currentRow} - /> . + +export type MatchType = 'contains' | 'starts_with' | 'ends_with' | 'is_exactly' | 'regex'; + +export interface PatternEntry { + id: string; + matchType: MatchType; + value: string; +} + +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +export function simplePatternToRegex(matchType: MatchType, value: string): string { + switch (matchType) { + case 'contains': + return `.*${escapeRegex(value)}.*`; + case 'starts_with': + return `^${escapeRegex(value)}.*`; + case 'ends_with': + return `.*${escapeRegex(value)}$`; + case 'is_exactly': + return `^${escapeRegex(value)}$`; + case 'regex': + return value; + } +} + +export function isValidRegex(pattern: string): boolean { + try { + new RegExp(pattern); + return true; + } catch { + return false; + } +} + +let _idCounter = 0; +export function newPatternId(): string { + return `pat_${++_idCounter}_${Date.now()}`; +} diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json index 997dcf8..f3d69db 100644 --- a/web/src/locales/ar.json +++ b/web/src/locales/ar.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "هل أنت متأكد من أنك تريد {{action}} هذا الحساب؟", "auth": "المصادقة", "authType": "نوع_المصادقة", - "autoConfiguring": "جارٍ التكوين التلقائي...", + "autoConfiguring": "جاري التكوين التلقائي…", + "autoDiscover": "اكتشاف إعدادات الخادم تلقائيًا", "autoDownloadNewMailboxes": "إضافة المجلدات الجديدة تلقائيًا", "autoDownloadNewMailboxesDescription": "إضافة المجلدات الجديدة المكتشفة تلقائيًا إلى قائمة التنزيل.", "beforeRelative": "تنزيل رسائل البريد الإلكتروني القديمة فقط", + "beforeRelativeDesc": "تنزيل الرسائل الأقدم من الفترة الزمنية المحددة فقط. يتحرك تاريخ القطع تلقائياً مع مرور الوقت — مفيد لأرشفة الرسائل القديمة تدriجياً.", "beforeRelativeValue": "تنزيل رسائل البريد الإلكتروني قبل {{value}} {{unit}} مضت", "cancelDownload": "إلغاء التنزيل", "cancelFailed": "فشل إلغاء مهمة التنزيل", @@ -124,9 +126,13 @@ "cronHour": "الساعة", "cronMinute": "الدقيقة", "cronMonday": "الإثنين", + "cronMonthAlignNote": "ملاحظة: إذا كان الشهر أقصر، فسيتم التنفيذ في اليوم الأخير من الشهر.", "cronMonthly": "شهرياً", "cronSaturday": "السبت", "cronSimple": "تعبير بسيط", + "cronSummaryDaily": "يومياً عند الساعة {{hour}}:{{minute}}", + "cronSummaryMonthly": "في اليوم {{day}} من كل شهر عند الساعة {{hour}}:{{minute}}", + "cronSummaryWeekly": "كل يوم {{day}} عند الساعة {{hour}}:{{minute}}", "cronSunday": "الأحد", "cronThursday": "الخميس", "cronTimezoneNote": "جميع الأوقات بالتوقيت المحلي للخادم.", @@ -145,6 +151,7 @@ "disableAccount": "تعطيل الحساب", "disabled": "معطل", "downloadAll": "تنزيل جميع رسائل البريد الإلكتروني", + "downloadAllDesc": "سيتم تنزيل جميع رسائل البريد الإلكتروني في كل صندوق بريد محدد، من أول رسالة إلى أحدثها. الأفضل للأرشفة الكاملة.", "downloadBatchSize": "حجم دفعة التنزيل", "downloadBatchSizeDescription": "عدد الرسائل التي يتم جلبها لكل طلب IMAP", "downloadCancelled": "تم إلغاء المهمة", @@ -176,6 +183,35 @@ "enterValue": "الرجاء إدخال قيمة", "everyMinutes": "كل {{minutes}} دقيقة", "field": "الحقل", + "filters": { + "addHeader": "إضافة رأس", + "addPattern": "إضافة نمط", + "enableFiltering": "تفعيل تصفية المحتوى", + "enableFilteringDesc": "عند التفعيل، سيتم أرشفة الرسائل التي تطابق القواعد أدناه فقط. عند التعطيل، يتم أرشفة جميع الرسائل.", + "exclude": "استبعاد", + "include": "تضمين", + "matchType": { + "contains": "يحتوي على", + "endsWith": "ينتهي بـ", + "isExactly": "يطابق تماماً", + "regex": "تعبير نمطي (Regex)", + "startsWith": "يبدأ بـ" + }, + "noExcludePatterns": "لا توجد أنماط استبعاد — لن يتم استبعاد أي شيء.", + "noIncludePatterns": "لا توجد أنماط تضمين — سيتم تضمين كل شيء.", + "noLimit": "بلا حد", + "noSpamHeaders": "لم يتم تكوين رؤوس البريد المزعج.", + "senderFilter": "فلتر المرسل", + "senderFilterHelp": "تصفية الرسائل بناءً على عنوان بريد المرسل. تحدد أنماط التضمين المرسِلين المقبولين، بينما ترفض أنماط الاستبعاد من يطابقها.", + "sizeLimit": "حد الحجم", + "sizeLimitDesc": "سيتم تخطي الرسائل التي تتجاوز هذا الحجم أثناء الأرشفة. اتركه فارغاً لعدم وضع حد.", + "skipLargerThan": "تخطى الرسائل الأكبر من", + "spamHeaders": "رؤوس البريد المزعج", + "spamHeadersHelp": "سيتم تخطي الرسائل التي تحتوي على أي من هذه الرؤوس بقيمة 'yes' أو 'true'. تظهر أدناه بعض الرؤوس الشائعة لكشف البريد المزعج.", + "subjectFilter": "فلتر الموضوع", + "subjectFilterHelp": "تصفية الرسائل بناءً على سطر الموضوع. يعمل بنفس طريقة تصفية المرسل.", + "suggestions": "اقتراحات (اضغط للإضافة):" + }, "fixed": "ثابت", "folderSync": { "autoSelectDescendants": "تحديد العناصر الفرعية تلقائيًا", @@ -284,9 +320,25 @@ "selectUnit": "اختر وحدة", "selectedMailboxes": "صناديق البريد المحددة", "serverConfiguration": "تكوين الخادم (IMAP)", + "settings": { + "download": "تنزيل", + "downloadDesc": "تكوين وقت وكيفية جلب رسائل البريد الإلكتروني من الخادم.", + "filters": "الفلاتر", + "filtersDesc": "التحكم في الرسائل التي يتم أرشفتها. عند تعطيل الفلترة، يتم حفظ جميع الرسائل.", + "general": "عام", + "generalDesc": "معلومات الحساب الأساسية والحالة.", + "newAccount": "حساب جديد", + "performance": "الأداء", + "schedule": "جدول المزامنة", + "scope": "نطاق المزامنة", + "server": "الخادم", + "serverDesc": "إعدادات اتصال IMAP والمصادقة." + }, "since": "منذ", "sinceFixed": "منذ تاريخ محدد", + "sinceFixedDesc": "تنزيل الرسائل المستلمة بعد التاريخ المحدد فقط، وتجاهل ما قبله.", "sinceRelative": "تنزيل رسائل البريد الإلكتروني الأخيرة فقط", + "sinceRelativeDesc": "تنزيل رسائل الفترة الأخيرة فقط (مثل آخر 3 أشهر). يتحرك تاريخ البدء تلقائياً مع مرور الوقت.", "sinceRelativeValue": "تنزيل رسائل البريد الإلكتروني من آخر", "startDownload": "بدء التنزيل", "state": "الحالة", diff --git a/web/src/locales/da.json b/web/src/locales/da.json index a505556..6fdbd00 100644 --- a/web/src/locales/da.json +++ b/web/src/locales/da.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "Er du sikker på, at du vil {{action}} denne konto?", "auth": "Godkendelse", "authType": "godkendelsestype", - "autoConfiguring": "Konfigurerer automatisk...", + "autoConfiguring": "Konfigurerer automatisk…", + "autoDiscover": "Find serverindstillinger automatisk", "autoDownloadNewMailboxes": "Tilføj automatisk nye mapper", "autoDownloadNewMailboxesDescription": "Føj automatisk nye mapper til downloadlisten.", "beforeRelative": "Download kun gamle e-mails", + "beforeRelativeDesc": "Download kun e-mails, der er ældre end den angivne periode. Skæringsdatoen flyttes automatisk – nyttigt til gradvis arkivering af gamle e-mails.", "beforeRelativeValue": "Download e-mails før {{value}} {{unit}} siden", "cancelDownload": "Annuller download", "cancelFailed": "Kunne ikke annullere download-opgave", @@ -124,9 +126,13 @@ "cronHour": "Time", "cronMinute": "Minut", "cronMonday": "Mandag", + "cronMonthAlignNote": "Bemærk: Hvis måneden er kortere, udføres den på månedens sidste dag.", "cronMonthly": "Månedligt", "cronSaturday": "Lørdag", "cronSimple": "Simpelt udtryk", + "cronSummaryDaily": "Hver dag kl. {{hour}}:{{minute}}", + "cronSummaryMonthly": "Den {{day}}. i hver måned kl. {{hour}}:{{minute}}", + "cronSummaryWeekly": "Hver {{day}} kl. {{hour}}:{{minute}}", "cronSunday": "Søndag", "cronThursday": "Torsdag", "cronTimezoneNote": "Alle tider er serverens lokale tid.", @@ -145,6 +151,7 @@ "disableAccount": "Deaktivér konto", "disabled": "Deaktiveret", "downloadAll": "Download alle e-mails", + "downloadAllDesc": "Alle e-mails i alle valgte postkasser vil blive downloadet. Bedst til komplet arkivering.", "downloadBatchSize": "Download batchstørrelse", "downloadBatchSizeDescription": "Antal beskeder hentet pr. IMAP-anmodning", "downloadCancelled": "Opgave annulleret", @@ -176,6 +183,35 @@ "enterValue": "Indtast en værdi", "everyMinutes": "hvert {{minutes}} minut", "field": "Felt", + "filters": { + "addHeader": "Tilføj header", + "addPattern": "Tilføj mønster", + "enableFiltering": "Aktiver indholdsfiltrering", + "enableFilteringDesc": "Når den er aktiveret, vil kun e-mails, der matcher reglerne nedenfor, blive arkiveret. Når den er deaktiveret, arkiveres alle e-mails.", + "exclude": "Ekskluder", + "include": "Inkluder", + "matchType": { + "contains": "Indeholder", + "endsWith": "Ender med", + "isExactly": "Er præcis", + "regex": "Regulært udtryk (Regex)", + "startsWith": "Begynder med" + }, + "noExcludePatterns": "Ingen ekskluderingsmønstre – intet er ekskluderet.", + "noIncludePatterns": "Ingen inkluderingsmønstre – alt er inkluderet.", + "noLimit": "Ingen begrænsning", + "noSpamHeaders": "Ingen spamheadere konfigureret.", + "senderFilter": "Afsenderfilter", + "senderFilterHelp": "Filtrer e-mails baseret på afsenderens adresse. Inkluderingsmønstre tillader afsendere, mens ekskluderingsmønstre afviser dem.", + "sizeLimit": "Størrelsesbegrænsning", + "sizeLimitDesc": "E-mails, der er større end denne størrelse, vil blive sprunget over under arkivering. Lad feltet være tomt for ingen begrænsning.", + "skipLargerThan": "Spring e-mails over, der er større end", + "spamHeaders": "Spamheadere", + "spamHeadersHelp": "E-mails med en af disse headere sat til 'yes' eller 'true' vil blive sprunget over. Almindelige spamheadere er foreslået nedenfor.", + "subjectFilter": "Emnefilter", + "subjectFilterHelp": "Filtrer e-mails baseret på emnelinjen. Fungerer på samme måde som afsenderfiltrering.", + "suggestions": "Forslag (klik for at tilføje):" + }, "fixed": "Fast", "folderSync": { "autoSelectDescendants": "Vælg efterkommere automatisk", @@ -284,9 +320,25 @@ "selectUnit": "Vælg en enhed", "selectedMailboxes": "Valgte postkasser", "serverConfiguration": "Serverkonfiguration (IMAP)", + "settings": { + "download": "Download", + "downloadDesc": "Konfigurer, hvornår og hvordan e-mails hentes fra serveren.", + "filters": "Filtre", + "filtersDesc": "Styr, hvilke e-mails der arkiveres. Når filtrering er deaktiveret, gemmes alle e-mails.", + "general": "Generelt", + "generalDesc": "Generelle kontooplysninger og status.", + "newAccount": "Ny konto", + "performance": "Ydeevne", + "schedule": "Tidsplan", + "scope": "Omfang", + "server": "Server", + "serverDesc": "IMAP-forbindelsesindstillinger og godkendelse." + }, "since": "siden", "sinceFixed": "Siden specifik dato", + "sinceFixedDesc": "Download kun e-mails modtaget efter den valgte dato. E-mails før denne dato ignoreres.", "sinceRelative": "Download kun seneste e-mails", + "sinceRelativeDesc": "Download kun e-mails fra den seneste periode (f.eks. de seneste 3 måneder). Startdatoen flyttes automatisk fremad.", "sinceRelativeValue": "Download e-mails fra de sidste", "startDownload": "Start download", "state": "Tilstand", diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 42080f2..0c0f42e 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "Möchten Sie dieses Konto wirklich {{action}}?", "auth": "Authentifizierung", "authType": "Authentifizierungstyp", - "autoConfiguring": "Automatische Konfiguration läuft...", + "autoConfiguring": "Automatische Konfiguration…", + "autoDiscover": "Servereinstellungen automatisch erkennen", "autoDownloadNewMailboxes": "Neue Ordner automatisch hinzufügen", "autoDownloadNewMailboxesDescription": "Neu entdeckte Ordner automatisch zur Download-Liste hinzufügen.", "beforeRelative": "Nur alte E-Mails herunterladen", + "beforeRelativeDesc": "Nur E-Mails herunterladen, die älter als der angegebene Zeitraum sind. Das Stichtagsdatum verschiebt sich automatisch – ideal für die schrittweise Archivierung.", "beforeRelativeValue": "E-Mails von vor {{value}} {{unit}} herunterladen", "cancelDownload": "Download abbrechen", "cancelFailed": "Download-Aufgabe konnte nicht abgebrochen werden", @@ -124,9 +126,13 @@ "cronHour": "Stunde", "cronMinute": "Minute", "cronMonday": "Montag", + "cronMonthAlignNote": "Hinweis: Bei kürzeren Monaten erfolgt die Ausführung am letzten Tag des Monats.", "cronMonthly": "Monatlich", "cronSaturday": "Samstag", "cronSimple": "Einfacher Ausdruck", + "cronSummaryDaily": "Täglich um {{hour}}:{{minute}} Uhr", + "cronSummaryMonthly": "Jeden {{day}}. des Monats um {{hour}}:{{minute}} Uhr", + "cronSummaryWeekly": "Wöchentlich, jeden {{day}} um {{hour}}:{{minute}} Uhr", "cronSunday": "Sonntag", "cronThursday": "Donnerstag", "cronTimezoneNote": "Alle Zeiten nutzen Server-Ortszeit.", @@ -145,6 +151,7 @@ "disableAccount": "Konto deaktivieren", "disabled": "Deaktiviert", "downloadAll": "Alle E-Mails herunterladen", + "downloadAllDesc": "Alle E-Mails in jedem ausgewählten Postfach werden heruntergeladen. Bestens geeignet für eine vollständige Archivierung.", "downloadBatchSize": "Download-Batch-Größe", "downloadBatchSizeDescription": "Anzahl der pro IMAP-Anfrage abgerufenen Nachrichten", "downloadCancelled": "Aufgabe abgebrochen", @@ -176,6 +183,35 @@ "enterValue": "Wert eingeben", "everyMinutes": "alle {{minutes}} Minuten", "field": "Feld", + "filters": { + "addHeader": "Header hinzufügen", + "addPattern": "Muster hinzufügen", + "enableFiltering": "Inhaltsfilterung aktivieren", + "enableFilteringDesc": "Wenn aktiviert, werden nur E-Mails archiviert, die den folgenden Regeln entsprechen. Wenn deaktiviert, werden alle E-Mails archiviert.", + "exclude": "Ausschließen", + "include": "Einschließen", + "matchType": { + "contains": "Enthält", + "endsWith": "Endet mit", + "isExactly": "Ist genau", + "regex": "Regulärer Ausdruck (Regex)", + "startsWith": "Beginnt mit" + }, + "noExcludePatterns": "Keine Ausschlussmuster – nichts wird ausgeschlossen.", + "noIncludePatterns": "Keine Einschlussmuster – alles wird eingeschlossen.", + "noLimit": "Kein Limit", + "noSpamHeaders": "Keine Spam-Header konfiguriert.", + "senderFilter": "Absenderfilter", + "senderFilterHelp": "Filtert E-Mails nach Absenderadresse. Einschlussmuster lassen Absender zu, Ausschlussmuster weisen übereinstimmende Absender ab.", + "sizeLimit": "Größenbeschränkung", + "sizeLimitDesc": "E-Mails, die diese Größe überschreiten, werden bei der Archivierung übersprungen. Leer lassen für unbegrenzt.", + "skipLargerThan": "E-Mails überspringen größer als", + "spamHeaders": "Spam-Header", + "spamHeadersHelp": "E-Mails, bei denen einer dieser Header auf 'yes' oder 'true' gesetzt ist, werden übersprungen. Häufige Spam-Header sind unten angegeben.", + "subjectFilter": "Betreffsfilter", + "subjectFilterHelp": "Filtert E-Mails nach Betreffzeile. Funktioniert genauso wie die Absenderfilterung.", + "suggestions": "Vorschläge (zum Hinzufügen anklicken):" + }, "fixed": "Fest", "folderSync": { "autoSelectDescendants": "Unterelemente automatisch auswählen", @@ -284,9 +320,25 @@ "selectUnit": "Einheit auswählen", "selectedMailboxes": "Ausgewählte Postfächer", "serverConfiguration": "Serverkonfiguration (IMAP)", + "settings": { + "download": "Herunterladen", + "downloadDesc": "Konfigurieren, wann und wie E-Mails vom Server abgerufen werden.", + "filters": "Filter", + "filtersDesc": "Steuern Sie, welche E-Mails archiviert werden. Wenn die Filterung deaktiviert ist, werden alle E-Mails gespeichert.", + "general": "Allgemein", + "generalDesc": "Basis-Kontoinformationen und Status.", + "newAccount": "Neues Konto", + "performance": "Leistung", + "schedule": "Zeitplan", + "scope": "Zeitraum", + "server": "Server", + "serverDesc": "IMAP-Verbindungseinstellungen und Authentifizierung." + }, "since": "seit", "sinceFixed": "Seit einem bestimmten Datum", + "sinceFixedDesc": "Nur E-Mails herunterladen, die nach dem gewählten Datum empfangen wurden. Ältere E-Mails werden ignoriert.", "sinceRelative": "Nur aktuelle E-Mails herunterladen", + "sinceRelativeDesc": "Nur E-Mails aus dem jüngsten Zeitraum herunterladen (z. B. letzte 3 Monate). Das Startdatum verschiebt sich automatisch.", "sinceRelativeValue": "E-Mails der letzten Zeit herunterladen", "startDownload": "Download starten", "state": "Zustand", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 2f13a09..64a29f6 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -98,11 +98,14 @@ "allMailSkipped": "Selected standard folders. 'All Mail' was skipped to avoid duplicates.", "areYouSureYouWantTo": "Are you sure you want to {{action}} this account?", "auth": "Auth", + "authPassword": "Password", "authType": "auth_type", "autoConfiguring": "Auto-configuring...", + "autoDiscover": "Auto-discover Server Settings", "autoDownloadNewMailboxes": "Auto-add new mailboxes", "autoDownloadNewMailboxesDescription": "Automatically add newly discovered folders to the download list.", "beforeRelative": "Download Old Emails Only", + "beforeRelativeDesc": "Only download emails older than the specified time period. The cutoff date automatically moves over time — useful for gradually archiving old emails while skipping recent ones.", "beforeRelativeValue": "Download emails before {{value}} {{unit}} ago", "cancelDownload": "Cancel download", "cancelFailed": "Failed to cancel download task", @@ -114,19 +117,24 @@ "clickSaveWhenDone": "Click save when you're done.", "continue": "Continue", "createdAt": "Created At", + "creating": "Creating...", "creationFailed": "Creation failed, please try again later", "cronAdvanced": "Advanced Expression", "cronDaily": "Daily", - "cronDayOfMonth": "Day of Week", + "cronDayOfMonth": "Day of Month", "cronDayOfWeek": "Day of Week", "cronFrequency": "Frequency", "cronFriday": "Friday", "cronHour": "Hour", "cronMinute": "Minute", "cronMonday": "Monday", + "cronMonthAlignNote": "Note: If the month is shorter, it will execute on the last day of the month.", "cronMonthly": "Monthly", "cronSaturday": "Saturday", "cronSimple": "Simple Expression", + "cronSummaryDaily": "Every day at {{hour}}:{{minute}}", + "cronSummaryMonthly": "On day {{day}} of every month at {{hour}}:{{minute}}", + "cronSummaryWeekly": "Every {{day}} at {{hour}}:{{minute}}", "cronSunday": "Sunday", "cronThursday": "Thursday", "cronTimezoneNote": "All times use server local timezone.", @@ -145,6 +153,7 @@ "disableAccount": "Disable account", "disabled": "Disabled", "downloadAll": "Download All Emails", + "downloadAllDesc": "All emails in every selected mailbox will be downloaded, from the very first to the latest. Best for complete archiving.", "downloadBatchSize": "Download batch size", "downloadBatchSizeDescription": "Number of messages fetched per IMAP request", "downloadCancelled": "Task cancelled", @@ -176,6 +185,35 @@ "enterValue": "Please enter a value", "everyMinutes": "every {{minutes}} minutes", "field": "Field", + "filters": { + "addHeader": "Add Header", + "addPattern": "Add pattern", + "enableFiltering": "Enable content filtering", + "enableFilteringDesc": "When enabled, only emails matching the rules below will be archived. When disabled, all emails are archived.", + "exclude": "Exclude", + "include": "Include", + "matchType": { + "contains": "Contains", + "endsWith": "Ends with", + "isExactly": "Is exactly", + "regex": "Regular expression (Regex)", + "startsWith": "Starts with" + }, + "noExcludePatterns": "No exclude patterns — nothing is excluded.", + "noIncludePatterns": "No include patterns — everything is included.", + "noLimit": "No limit", + "noSpamHeaders": "No spam headers configured.", + "senderFilter": "Sender Filter", + "senderFilterHelp": "Filter emails based on the sender's email address. Include patterns determine which senders pass; exclude patterns reject matching senders.", + "sizeLimit": "Size Limit", + "sizeLimitDesc": "Emails larger than this size will be skipped during archiving. Leave empty for no size limit.", + "skipLargerThan": "Skip emails larger than", + "spamHeaders": "Spam Headers", + "spamHeadersHelp": "Emails with any of these headers set to 'yes' or 'true' will be skipped. Common spam detection headers are suggested below.", + "subjectFilter": "Subject Filter", + "subjectFilterHelp": "Filter emails based on the subject line. Works the same way as sender filtering.", + "suggestions": "Suggestions (click to add):" + }, "fixed": "Fixed", "folderSync": { "autoSelectDescendants": "Auto select descendants", @@ -284,9 +322,33 @@ "selectUnit": "Select a unit", "selectedMailboxes": "Selected Mailboxes", "serverConfiguration": "Server Configuration (IMAP)", + "settings": { + "backToAccounts": "Back to Accounts", + "download": "Download", + "downloadDesc": "Configure when and how emails are fetched from the server.", + "filters": "Filters", + "filtersDesc": "Control which emails are archived. When filtering is disabled, all emails are saved.", + "general": "General", + "generalDesc": "Basic account information and status.", + "loading": "Loading account...", + "newAccount": "New Account", + "performance": "Performance", + "reset": "Reset", + "save": "Save", + "saved": "Saved", + "savedDesc": "Settings have been saved successfully.", + "saving": "Saving...", + "schedule": "Schedule", + "scope": "Scope", + "server": "Server", + "serverDesc": "IMAP connection settings and authentication.", + "settings": "Settings" + }, "since": "since", "sinceFixed": "Since Specific Date", + "sinceFixedDesc": "Only download emails received after the selected date. Emails before that date will be ignored.", "sinceRelative": "Download Recent Emails Only", + "sinceRelativeDesc": "Only download emails from the recent period (e.g. last 3 months). The start date automatically moves forward over time.", "sinceRelativeValue": "Download emails from the last", "startDownload": "Start download", "state": "State", diff --git a/web/src/locales/es.json b/web/src/locales/es.json index 3755cf3..99e59ac 100644 --- a/web/src/locales/es.json +++ b/web/src/locales/es.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "¿Está seguro de que desea {{action}} esta cuenta?", "auth": "Autenticación", "authType": "tipo de autenticación", - "autoConfiguring": "Autoconfigurando...", + "autoConfiguring": "Configurando automáticamente…", + "autoDiscover": "Detectar automáticamente la configuración del servidor", "autoDownloadNewMailboxes": "Añadir automáticamente nuevas carpetas", "autoDownloadNewMailboxesDescription": "Añadir automáticamente las nuevas carpetas a la lista de descarga.", "beforeRelative": "Descargar solo correos antiguos", + "beforeRelativeDesc": "Solo descargar correos anteriores al período especificado. La fecha límite avanza automáticamente, ideal para archivar correos viejos gradualmente.", "beforeRelativeValue": "Descargar correos de hace {{value}} {{unit}}", "cancelDownload": "Cancelar descarga", "cancelFailed": "Error al cancelar la tarea de descarga", @@ -124,9 +126,13 @@ "cronHour": "Hora", "cronMinute": "Minuto", "cronMonday": "Lunes", + "cronMonthAlignNote": "Nota: Si el mes es más corto, se ejecutará el último día del mes.", "cronMonthly": "Mensual", "cronSaturday": "Sábado", "cronSimple": "Expresión simple", + "cronSummaryDaily": "Todos los días a las {{hour}}:{{minute}}", + "cronSummaryMonthly": "El día {{day}} de cada mes a las {{hour}}:{{minute}}", + "cronSummaryWeekly": "Todos los {{day}} a las {{hour}}:{{minute}}", "cronSunday": "Domingo", "cronThursday": "Jueves", "cronTimezoneNote": "Horas en zona horaria del servidor.", @@ -145,6 +151,7 @@ "disableAccount": "Desactivar cuenta", "disabled": "Deshabilitado", "downloadAll": "Descargar todos los correos", + "downloadAllDesc": "Se descargarán todos los correos de cada buzón seleccionado. Ideal para un archivado completo.", "downloadBatchSize": "Tamaño del lote de descarga", "downloadBatchSizeDescription": "Número de mensajes obtenidos por solicitud IMAP", "downloadCancelled": "Tarea cancelada", @@ -176,6 +183,35 @@ "enterValue": "Introduce valor", "everyMinutes": "cada {{minutes}} minutos", "field": "Campo", + "filters": { + "addHeader": "Añadir encabezado", + "addPattern": "Añadir patrón", + "enableFiltering": "Activar filtrado de contenido", + "enableFilteringDesc": "Si está activado, solo se archivarán los correos que cumplan las siguientes reglas. Si está desactivado, se archivarán todos.", + "exclude": "Excluir", + "include": "Incluir", + "matchType": { + "contains": "Contiene", + "endsWith": "Termina en", + "isExactly": "Es exactamente", + "regex": "Expresión regular (Regex)", + "startsWith": "Empieza por" + }, + "noExcludePatterns": "Sin patrones de exclusión: no se excluye nada.", + "noIncludePatterns": "Sin patrones de inclusión: se incluye todo.", + "noLimit": "Sin límite", + "noSpamHeaders": "No hay encabezados de spam configurados.", + "senderFilter": "Filtro de remitente", + "senderFilterHelp": "Filtra correos por dirección del remitente. Los patrones de inclusión permiten el paso; los de exclusión rechazan coincidencias.", + "sizeLimit": "Límite de tamaño", + "sizeLimitDesc": "Los correos que superen este tamaño se omitirán al archivar. Déjelo vacío si no desea un límite.", + "skipLargerThan": "Omitir correos mayores de", + "spamHeaders": "Encabezados de spam", + "spamHeadersHelp": "Se omitirán los correos con cualquiera de estos encabezados establecidos en 'yes' o 'true'. Abajo se sugieren encabezados comunes de detección de spam.", + "subjectFilter": "Filtro de asunto", + "subjectFilterHelp": "Filtra correos por la línea de asunto. Funciona de la misma manera que el filtrado de remitentes.", + "suggestions": "Sugerencias (haz clic para añadir):" + }, "fixed": "Fija", "folderSync": { "autoSelectDescendants": "Seleccionar automáticamente los descendientes", @@ -284,9 +320,25 @@ "selectUnit": "Seleccionar unidad", "selectedMailboxes": "Buzones seleccionados", "serverConfiguration": "Configuración del servidor (IMAP)", + "settings": { + "download": "Descarga", + "downloadDesc": "Configure cuándo y cómo se obtienen los correos del servidor.", + "filters": "Filtros", + "filtersDesc": "Controle qué correos se archivan. Si el filtrado está desactivado, se guardarán todos.", + "general": "General", + "generalDesc": "Información básica de la cuenta y estado.", + "newAccount": "Nueva cuenta", + "performance": "Rendimiento", + "schedule": "Planificación", + "scope": "Alcance", + "server": "Servidor", + "serverDesc": "Configuración de conexión IMAP y autenticación." + }, "since": "desde", "sinceFixed": "Desde una fecha específica", + "sinceFixedDesc": "Solo descargar correos posteriores a la fecha seleccionada. Los anteriores serán ignorados.", "sinceRelative": "Descargar solo correos recientes", + "sinceRelativeDesc": "Solo descargar correos del período reciente (ej. últimos 3 meses). La fecha de inicio avanza automáticamente.", "sinceRelativeValue": "Descargar correos de los últimos", "startDownload": "Iniciar descarga", "state": "Estado", diff --git a/web/src/locales/fi.json b/web/src/locales/fi.json index 722b6dd..dbe9725 100644 --- a/web/src/locales/fi.json +++ b/web/src/locales/fi.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "Haluatko varmasti {{action}} tämän tilin?", "auth": "Todennus", "authType": "todennustyyppi", - "autoConfiguring": "Automaattinen määritys...", + "autoConfiguring": "Määritetään automaattisesti…", + "autoDiscover": "Hae palvelinasetukset automaattisesti", "autoDownloadNewMailboxes": "Lisää uudet kansiot automaattisesti", "autoDownloadNewMailboxesDescription": "Lisää uudet löydetyt kansiot automaattisesti latausluetteloon.", "beforeRelative": "Lataa vain vanhat sähköpostit", + "beforeRelativeDesc": "Lataa vain määritettyä ajanjaksoa vanhemmat sähköpostit. Takaraja siirtyy automaattisesti – hyödyllinen vanhojen sähköpostien asteittaiseen arkistointiin.", "beforeRelativeValue": "Lataa sähköpostit {{value}} {{unit}} sitten", "cancelDownload": "Peruuta lataus", "cancelFailed": "Lataustehtävän peruuttaminen epäonnistui", @@ -124,9 +126,13 @@ "cronHour": "Tunti", "cronMinute": "Minuutti", "cronMonday": "Maanantai", + "cronMonthAlignNote": "Huomautus: Jos kuukausi on lyhyempi, suoritus tapahtuu kuukauden viimeisenä päivänä.", "cronMonthly": "Kuukausittain", "cronSaturday": "Lauantai", "cronSimple": "Yksinkertainen lauseke", + "cronSummaryDaily": "Joka päivä klo {{hour}}:{{minute}}", + "cronSummaryMonthly": "Joka kuukauden {{day}}. päivä klo {{hour}}:{{minute}}", + "cronSummaryWeekly": "Joka {{day}} klo {{hour}}:{{minute}}", "cronSunday": "Sunnuntai", "cronThursday": "Torstai", "cronTimezoneNote": "Kaikki ajat palvelimen paikallista aikaa.", @@ -145,6 +151,7 @@ "disableAccount": "Poista tili käytöstä", "disabled": "Poistettu käytöstä", "downloadAll": "Lataa kaikki sähköpostit", + "downloadAllDesc": "Kaikki sähköpostit valituista postilaatikoista ladataan. Paras täydelliseen arkistointiin.", "downloadBatchSize": "Latauserän koko", "downloadBatchSizeDescription": "IMAP-pyyntöä kohden noudettujen viestien määrä", "downloadCancelled": "Tehtävä peruutettu", @@ -176,6 +183,35 @@ "enterValue": "Syötä arvo", "everyMinutes": "joka {{minutes}} minuutti", "field": "Kenttä", + "filters": { + "addHeader": "Lisää otsake", + "addPattern": "Lisää kuvio", + "enableFiltering": "Ota sisällön suodatus käyttöön", + "enableFilteringDesc": "Kun käytössä, vain alla olevia sääntöjä vastaavat sähköpostit arkistoidaan. Kun poissa käytöstä, kaikki sähköpostit arkistoidaan.", + "exclude": "Poissulje", + "include": "Sisällytä", + "matchType": { + "contains": "Sisältää", + "endsWith": "Päättyy merkkeihin", + "isExactly": "On täsmälleen", + "regex": "Säännöllinen lauseke (Regex)", + "startsWith": "Alkaa merkeillä" + }, + "noExcludePatterns": "Ei poissulkemiskuvioita – mitään ei poissuljeta.", + "noIncludePatterns": "Ei sisällytyskuvioita – kaikki sisällytetään.", + "noLimit": "Ei rajoitusta", + "noSpamHeaders": "Spam-otsakkeita ei ole määritetty.", + "senderFilter": "Lähettäjän suodatin", + "senderFilterHelp": "Suodata sähköpostit lähettäjän osoitteen perusteun. Sisällytyskuviot sallivat lähettäjät, poissulkemiskuviot hylkäävät ne.", + "sizeLimit": "Koko-rajoitus", + "sizeLimitDesc": "Tätä suuremmat sähköpostit ohitetaan arkistoinnin aikana. Jätä tyhjäksi, jos et halua kokorajoitusta.", + "skipLargerThan": "Ohita sähköpostit, jotka ovat suurempia kuin", + "spamHeaders": "Spam-otsakkeet", + "spamHeadersHelp": "Sähköpostit, joiden otsakkeena on 'yes' tai 'true', ohitetaan. Yleisimmät roskapostin tunnistusotsakkeet näkyvät alla.", + "subjectFilter": "Aiheen suodatin", + "subjectFilterHelp": "Suodata sähköpostit aiherivin perusteella. Toimii samalla tavalla kuin lähettäjän suodatus.", + "suggestions": "Ehdotukset (klikkaa lisätäksesi):" + }, "fixed": "Kiinteä", "folderSync": { "autoSelectDescendants": "Valitse alisolmut automaattisesti", @@ -284,9 +320,25 @@ "selectUnit": "Valitse yksikkö", "selectedMailboxes": "Valitut postilaatikot", "serverConfiguration": "Palvelinmääritys (IMAP)", + "settings": { + "download": "Lataus", + "downloadDesc": "Määritä, milloin ja miten sähköpostit haetaan palvelimelta.", + "filters": "Suodattimet", + "filtersDesc": "Hallitse, mitkä sähköpostit arkistoidaan. Kun suodatus on poissa päältä, kaikki sähköpostit tallennetaan.", + "general": "Yleiset", + "generalDesc": "Tilin perustiedot ja tila.", + "newAccount": "Uusi tili", + "performance": "Suorituskyky", + "schedule": "Aikataulu", + "scope": "Laajuus", + "server": "Palvelin", + "serverDesc": "IMAP-yhteysasetukset ja todennus." + }, "since": "alkaen", "sinceFixed": "Tietystä päivämäärästä lähtien", + "sinceFixedDesc": "Lataa vain valitun päivämäärän jälkeen saapuneet sähköpostit. Sitä edeltävät sähköpostit ohitetaan.", "sinceRelative": "Lataa vain viimeisimmät sähköpostit", + "sinceRelativeDesc": "Lataa vain viimeaikaiset sähköpostit (esim. viimeiset 3 kuukautta). Aloituspäivämäärä siirtyy automaattisesti eteenpäin.", "sinceRelativeValue": "Lataa sähköpostit viimeisimmiltä", "startDownload": "Aloita lataus", "state": "Tila", diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json index fdf7cd9..92cc4ce 100644 --- a/web/src/locales/fr.json +++ b/web/src/locales/fr.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "Êtes-vous sûr de vouloir {{action}} ce compte ?", "auth": "Auth.", "authType": "type_auth", - "autoConfiguring": "Configuration automatique...", + "autoConfiguring": "Configuration automatique…", + "autoDiscover": "Détection automatique des paramètres du serveur", "autoDownloadNewMailboxes": "Ajouter automatiquement les nouveaux dossiers", "autoDownloadNewMailboxesDescription": "Ajouter automatiquement les nouveaux dossiers à la liste de téléchargement.", "beforeRelative": "Télécharger uniquement les anciens e-mails", + "beforeRelativeDesc": "Télécharger uniquement les e-mails antérieurs à la période spécifiée. La date limite s'ajuste automatiquement, idéal pour archiver progressivement les anciens e-mails.", "beforeRelativeValue": "Télécharger les e-mails d'il y a {{value}} {{unit}}", "cancelDownload": "Annuler le téléchargement", "cancelFailed": "Échec de l'annulation de la tâche de téléchargement", @@ -124,9 +126,13 @@ "cronHour": "Heure", "cronMinute": "Minute", "cronMonday": "Lundi", + "cronMonthAlignNote": "Remarque : Si le mois est plus court, l'exécution aura lieu le dernier jour du mois.", "cronMonthly": "Chaque mois", "cronSaturday": "Samedi", "cronSimple": "Expression simple", + "cronSummaryDaily": "Chaque jour à {{hour}}:{{minute}}", + "cronSummaryMonthly": "Le {{day}} de chaque mois à {{hour}}:{{minute}}", + "cronSummaryWeekly": "Chaque {{day}} à {{hour}}:{{minute}}", "cronSunday": "Dimanche", "cronThursday": "Jeudi", "cronTimezoneNote": "Heures au fuseau horaire du serveur.", @@ -145,6 +151,7 @@ "disableAccount": "Désactiver le compte", "disabled": "Désactivé", "downloadAll": "Télécharger tous les e-mails", + "downloadAllDesc": "Tous les e-mails des boîtes aux lettres sélectionnées seront téléchargés. Idéal pour un archivage complet.", "downloadBatchSize": "Taille du lot de téléchargement", "downloadBatchSizeDescription": "Nombre de messages récupérés par requête IMAP", "downloadCancelled": "Tâche annulée", @@ -176,6 +183,35 @@ "enterValue": "Entrez une valeur", "everyMinutes": "toutes les {{minutes}} minutes", "field": "Champ", + "filters": { + "addHeader": "Ajouter un en-tête", + "addPattern": "Ajouter un motif", + "enableFiltering": "Activer le filtrage du contenu", + "enableFilteringDesc": "Lorsqu'il est activé, seuls les e-mails correspondant aux règles ci-dessous seront archivés. S'il est désactivé, tous les e-mails seront archivés.", + "exclude": "Exclure", + "include": "Inclure", + "matchType": { + "contains": "Contient", + "endsWith": "Se termine par", + "isExactly": "Est exactement", + "regex": "Expression régulière (Regex)", + "startsWith": "Commence par" + }, + "noExcludePatterns": "Aucun motif d'exclusion — rien n'est exclu.", + "noIncludePatterns": "Aucun motif d'inclusion — tout est inclus.", + "noLimit": "Sans limite", + "noSpamHeaders": "Aucun en-tête de spam configuré.", + "senderFilter": "Filtre d'expéditeur", + "senderFilterHelp": "Filtrez par adresse de l'expéditeur. Les motifs d'inclusion autorisent les expéditeurs ; les motifs d'exclusion les rejettent.", + "sizeLimit": "Limite de taille", + "sizeLimitDesc": "Les e-mails dépassant cette taille seront ignorés lors de l'archivage. Laissez vide pour ne pas fixer de limite.", + "skipLargerThan": "Ignorer les e-mails supérieurs à", + "spamHeaders": "En-têtes de spam", + "spamHeadersHelp": "Les e-mails contenant l'un de ces en-têtes défini sur 'yes' ou 'true' seront ignorés. Des en-têtes de détection de spam courants sont suggérés ci-dessous.", + "subjectFilter": "Filtre d'objet", + "subjectFilterHelp": "Filtrez les e-mails en fonction de l'objet. Fonctionne de la même manière que le filtrage des expéditeurs.", + "suggestions": "Suggestions (cliquer pour ajouter) :" + }, "fixed": "Fixe", "folderSync": { "autoSelectDescendants": "Sélectionner automatiquement les descendants", @@ -284,9 +320,25 @@ "selectUnit": "Sélectionner une unité", "selectedMailboxes": "Boîtes mail sélectionnées", "serverConfiguration": "Configuration du Serveur (IMAP)", + "settings": { + "download": "Téléchargement", + "downloadDesc": "Configurer quand et comment les e-mails sont récupérés depuis le serveur.", + "filters": "Filtres", + "filtersDesc": "Contrôlez quels e-mails sont archivés. Si le filtrage est désactivé, tous les e-mails seront enregistrés.", + "general": "Général", + "generalDesc": "Informations de base sur le compte et statut.", + "newAccount": "Nouveau compte", + "performance": "Performances", + "schedule": "Planification", + "scope": "Période", + "server": "Serveur", + "serverDesc": "Paramètres de connexion IMAP et authentification." + }, "since": "depuis", "sinceFixed": "Depuis une date spécifique", + "sinceFixedDesc": "Télécharger uniquement les e-mails reçus après la date sélectionnée. Les e-mails antérieurs seront ignorés.", "sinceRelative": "Télécharger uniquement les e-mails récents", + "sinceRelativeDesc": "Télécharger uniquement les e-mails récents (ex. 3 derniers mois). La date de début avance automatiquement.", "sinceRelativeValue": "Télécharger les e-mails des derniers", "startDownload": "Lancer le téléchargement", "state": "État", diff --git a/web/src/locales/it.json b/web/src/locales/it.json index 3b64a37..6579e19 100644 --- a/web/src/locales/it.json +++ b/web/src/locales/it.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "Sei sicuro di voler {{action}} questo account?", "auth": "Autenticazione", "authType": "tipo_autenticazione", - "autoConfiguring": "Configurazione automatica...", + "autoConfiguring": "Configurazione automatica…", + "autoDiscover": "Rilevamento automatico impostazioni server", "autoDownloadNewMailboxes": "Aggiungi automaticamente nuove cartelle", "autoDownloadNewMailboxesDescription": "Aggiungi automaticamente le nuove cartelle all'elenco di download.", "beforeRelative": "Scarica solo le vecchie email", + "beforeRelativeDesc": "Scarica solo le email antecedenti al periodo specificato. La data limite si aggiorna automaticamente, utile per archiviare gradualmente le vecchie email.", "beforeRelativeValue": "Scarica email da {{value}} {{unit}} fa", "cancelDownload": "Annulla download", "cancelFailed": "Annullamento attività di download non riuscito", @@ -124,9 +126,13 @@ "cronHour": "Ora", "cronMinute": "Minuto", "cronMonday": "Lunedì", + "cronMonthAlignNote": "Nota: Se il mese è più breve, verrà eseguito l'ultimo giorno del mese.", "cronMonthly": "Ogni mese", "cronSaturday": "Sabato", "cronSimple": "Espressione semplice", + "cronSummaryDaily": "Ogni giorno alle {{hour}}:{{minute}}", + "cronSummaryMonthly": "Il giorno {{day}} di ogni mese alle {{hour}}:{{minute}}", + "cronSummaryWeekly": "Ogni {{day}} alle {{hour}}:{{minute}}", "cronSunday": "Domenica", "cronThursday": "Giovedì", "cronTimezoneNote": "Orari nel fuso orario del server.", @@ -145,6 +151,7 @@ "disableAccount": "Disattiva account", "disabled": "Disabilitato", "downloadAll": "Scarica tutte le email", + "downloadAllDesc": "Tutte le email in ogni casella di posta selezionata verranno scaricate. Ideale per un archivio completo.", "downloadBatchSize": "Dimensione del lotto di download", "downloadBatchSizeDescription": "Numero di messaggi recuperati per richiesta IMAP", "downloadCancelled": "Attività annullata", @@ -176,6 +183,35 @@ "enterValue": "Inserisci un valore", "everyMinutes": "ogni {{minutes}} minuti", "field": "Campo", + "filters": { + "addHeader": "Aggiungi intestazione", + "addPattern": "Aggiungi pattern", + "enableFiltering": "Attiva il filtraggio dei contenuti", + "enableFilteringDesc": "Se attivato, verranno archiviate solo le email che soddisfano le regole sottostanti. Se disattivato, verranno archiviate tutte le email.", + "exclude": "Escludi", + "include": "Includi", + "matchType": { + "contains": "Contiene", + "endsWith": "Termina con", + "isExactly": "È esattamente", + "regex": "Espressione regolare (Regex)", + "startsWith": "Inizia con" + }, + "noExcludePatterns": "Nessun pattern di esclusione — nulla è escluso.", + "noIncludePatterns": "Nessun pattern di inclusione — tutto è incluso.", + "noLimit": "Nessun limite", + "noSpamHeaders": "Nessuna intestazione spam configurata.", + "senderFilter": "Filtro mittente", + "senderFilterHelp": "Filtra le email in base all'indirizzo del mittente. I pattern di inclusione approvano i mittenti, quelli di esclusione li rifiutano.", + "sizeLimit": "Limite di dimensione", + "sizeLimitDesc": "Le email che superano questa dimensione verranno ignorate durante l'archiviazione. Lascia vuoto per nessun limite.", + "skipLargerThan": "Salta le email più grandi di", + "spamHeaders": "Intestazioni spam", + "spamHeadersHelp": "Le email con una di queste intestazioni impostata su 'yes' o 'true' verranno ignorate. Di seguito sono suggerite le intestazioni spam più comuni.", + "subjectFilter": "Filtro oggetto", + "subjectFilterHelp": "Filtra le email in base all'oggetto. Funziona allo stesso modo del filtraggio del mittente.", + "suggestions": "Suggerimenti (clicca per aggiungere):" + }, "fixed": "Fissa", "folderSync": { "autoSelectDescendants": "Seleziona automaticamente i discendenti", @@ -284,9 +320,25 @@ "selectUnit": "Seleziona un'unità", "selectedMailboxes": "Caselle selezionate", "serverConfiguration": "Configurazione Server (IMAP)", + "settings": { + "download": "Download", + "downloadDesc": "Configura quando e come le email vengono scaricate dal server.", + "filters": "Filtri", + "filtersDesc": "Controlla quali email archiviare. Quando il filtraggio è disattivato, vengono salvate tutte le email.", + "general": "Generale", + "generalDesc": "Informazioni di base sull'account e stato.", + "newAccount": "Nuovo account", + "performance": "Prestazioni", + "schedule": "Pianificazione", + "scope": "Ambito", + "server": "Server", + "serverDesc": "Impostazioni di connessione IMAP e autenticazione." + }, "since": "da", "sinceFixed": "Da una data specifica", + "sinceFixedDesc": "Scarica solo le email ricevute dopo la date selezionata. Le email precedenti saranno ignorate.", "sinceRelative": "Scarica solo le email recenti", + "sinceRelativeDesc": "Scarica solo le email del periodo recente (es. ultimi 3 mesi). La data di inizio si aggiorna automaticamente.", "sinceRelativeValue": "Scarica email degli ultimi", "startDownload": "Avvia download", "state": "Stato", diff --git a/web/src/locales/jp.json b/web/src/locales/jp.json index 30dc60d..aae12e1 100644 --- a/web/src/locales/jp.json +++ b/web/src/locales/jp.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "本当にこのアカウントを{{action}}しますか?", "auth": "認証", "authType": "認証タイプ", - "autoConfiguring": "自動設定中...", + "autoConfiguring": "自動設定中…", + "autoDiscover": "サーバー設定の自動検出", "autoDownloadNewMailboxes": "新着フォルダーの自動追加", "autoDownloadNewMailboxesDescription": "新しく見つかったフォルダーを自動的にダウンロード一覧に追加します。", "beforeRelative": "古いメールのみダウンロード", + "beforeRelativeDesc": "指定した期間より古いメールのみをダウンロードします。基準日は自動的に推移し、古いメールの段階的なアーカイブに役立ちます。", "beforeRelativeValue": "{{value}} {{unit}}前より古いメールをダウンロード", "cancelDownload": "ダウンロードをキャンセル", "cancelFailed": "ダウンロードタスクのキャンセルに失敗しました", @@ -124,9 +126,13 @@ "cronHour": "时", "cronMinute": "分", "cronMonday": "月曜日", + "cronMonthAlignNote": "注意:指定した日付がその月にない場合は、月末の最終日に実行されます。", "cronMonthly": "毎月", "cronSaturday": "土曜日", "cronSimple": "簡易式", + "cronSummaryDaily": "毎日 {{hour}}:{{minute}}", + "cronSummaryMonthly": "毎月 {{day}} 日 {{hour}}:{{minute}}", + "cronSummaryWeekly": "毎週{{day}} {{hour}}:{{minute}}", "cronSunday": "日曜日", "cronThursday": "木曜日", "cronTimezoneNote": "時間はサーバーの現地時間です。", @@ -145,6 +151,7 @@ "disableAccount": "アカウントを無効化", "disabled": "無効", "downloadAll": "すべてのメールをダウンロード", + "downloadAllDesc": "選択されたメールボックスの全メールを最初から最新までダウンロードします。完全なアーカイブに最適です。", "downloadBatchSize": "ダウンロードバッチサイズ", "downloadBatchSizeDescription": "IMAPリクエストごとに取得されるメッセージ数", "downloadCancelled": "タスクをキャンセルしました", @@ -176,6 +183,35 @@ "enterValue": "値を入力してください", "everyMinutes": "{{minutes}}分ごと", "field": "フィールド", + "filters": { + "addHeader": "ヘッダーを追加", + "addPattern": "パターンを追加", + "enableFiltering": "コンテンツフィルターを有効にする", + "enableFilteringDesc": "有効にすると、以下のルールに一致するメールのみがアーカイブされます。無効にすると、すべてのメールがアーカイブされます。", + "exclude": "除外する", + "include": "含める", + "matchType": { + "contains": "含む", + "endsWith": "後方一致", + "isExactly": "完全一致", + "regex": "正規表現 (Regex)", + "startsWith": "前方一致" + }, + "noExcludePatterns": "除外するパターンがありません — 何も除外されません。", + "noIncludePatterns": "含めるパターンがありません — すべて対象になります。", + "noLimit": "制限なし", + "noSpamHeaders": "スパムヘッダーが設定されていません。", + "senderFilter": "送信者フィルター", + "senderFilterHelp": "送信者のメールアドレスでフィルターします。一致する「含めるパターン」は許可され、「除外するパターン」は拒否されます。", + "sizeLimit": "サイズ制限", + "sizeLimitDesc": "このサイズを超えるメールはアーカイブ時にスキップされます。制限しない場合は空欄にしてください。", + "skipLargerThan": "次より大きいサイズをスキップ:", + "spamHeaders": "スパムヘッダー", + "spamHeadersHelp": "これらのヘッダーのいずれかが 'yes' または 'true' に設定されているメールはスキップされます。以下に一般的なスパム検出ヘッダーを提案しています。", + "subjectFilter": "件名フィルター", + "subjectFilterHelp": "メールの件名でフィルターします。送信者フィルターと同じ仕組みで動作します。", + "suggestions": "推奨設定(クリックで追加):" + }, "fixed": "固定", "folderSync": { "autoSelectDescendants": "子項目を自動選択", @@ -284,9 +320,25 @@ "selectUnit": "単位を選択", "selectedMailboxes": "選択されたメールボックス", "serverConfiguration": "サーバー設定 (IMAP)", + "settings": { + "download": "ダウンロード設定", + "downloadDesc": "サーバーからメールを取得するタイミングと方法を設定します。", + "filters": "フィルター", + "filtersDesc": "アーカイブ対象のメールを制御します。フィルターを無効にすると、すべてのメールが保存されます。", + "general": "基本情報", + "generalDesc": "アカウントの基本情報とステータス。", + "newAccount": "新規アカウント", + "performance": "パフォーマンス", + "schedule": "時間計画", + "scope": "同期対象期間", + "server": "サーバー設定", + "serverDesc": "IMAP接続設定と認証。" + }, "since": "以降", "sinceFixed": "指定した日付以降", + "sinceFixedDesc": "選択した日付以降に受信したメールのみをダウンロードし、それ以前のメールは無視します。", "sinceRelative": "最近のメールのみダウンロード", + "sinceRelativeDesc": "直近の期間(例:過去3ヶ月)のメールのみをダウンロードします。開始日は時間経過に伴い自動的に更新されます。", "sinceRelativeValue": "直近の期間のメールをダウンロード", "startDownload": "ダウンロードを開始", "state": "状態", diff --git a/web/src/locales/ko.json b/web/src/locales/ko.json index 170b4de..11be0ed 100644 --- a/web/src/locales/ko.json +++ b/web/src/locales/ko.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "이 계정을 정말 {{action}}하시겠습니까?", "auth": "인증", "authType": "인증 유형", - "autoConfiguring": "자동 구성 중...", + "autoConfiguring": "자동 설정 중…", + "autoDiscover": "서버 설정 자동 검색", "autoDownloadNewMailboxes": "새 폴더 자동 추가", "autoDownloadNewMailboxesDescription": "새로 발견된 폴더를 다운로드 목록에 자동으로 추가합니다.", "beforeRelative": "이전 이메일만 다운로드", + "beforeRelativeDesc": "지정한 기간보다 오래된 이메일만 다운로드합니다. 기준 날짜는 자동으로 이동하며, 오래된 이메일을 순차적으로 보관할 때 유용합니다.", "beforeRelativeValue": "{{value}} {{unit}} 전의 이메일 다운로드", "cancelDownload": "다운로드 취소", "cancelFailed": "다운로드 작업 취소 실패", @@ -124,9 +126,13 @@ "cronHour": "시", "cronMinute": "분", "cronMonday": "월요일", + "cronMonthAlignNote": "참고: 해당 날짜가 없는 짧은 달의 경우, 해당 월의 마지막 날에 실행됩니다.", "cronMonthly": "매월", "cronSaturday": "토요일", "cronSimple": "간단한 표현식", + "cronSummaryDaily": "매일 {{hour}}:{{minute}}", + "cronSummaryMonthly": "매월 {{day}}일 {{hour}}:{{minute}}", + "cronSummaryWeekly": "매주 {{day}} {{hour}}:{{minute}}", "cronSunday": "일요일", "cronThursday": "목요일", "cronTimezoneNote": "모든 시간은 서버 현지 시간 기준입니다.", @@ -145,6 +151,7 @@ "disableAccount": "계정 비활성화", "disabled": "비활성화", "downloadAll": "모든 이메일 다운로드", + "downloadAllDesc": "선택한 사서함의 모든 이메일을 처음부터 끝까지 다운로드합니다. 전체 보관에 가장 적합합니다.", "downloadBatchSize": "다운로드 일괄 처리 크기", "downloadBatchSizeDescription": "IMAP 요청당 가져온 메시지 수", "downloadCancelled": "작업 취소됨", @@ -176,6 +183,35 @@ "enterValue": "값 입력", "everyMinutes": "매 {{minutes}}분", "field": "필드", + "filters": { + "addHeader": "헤더 추가", + "addPattern": "패턴 추가", + "enableFiltering": "콘텐츠 필터링 활성화", + "enableFilteringDesc": "활성화하면 아래 규칙과 일치하는 이메일만 보관됩니다. 비활성화하면 모든 이메일이 보관됩니다.", + "exclude": "제외", + "include": "포함", + "matchType": { + "contains": "포함", + "endsWith": "끝 문자열", + "isExactly": "정확히 일치", + "regex": "정규 표현식 (Regex)", + "startsWith": "시작 문자열" + }, + "noExcludePatterns": "제외 패턴 없음 — 제외되는 항목이 없습니다.", + "noIncludePatterns": "포함 패턴 없음 — 모든 항목이 포함됩니다.", + "noLimit": "제한 없음", + "noSpamHeaders": "설정된 스팸 헤더가 없습니다.", + "senderFilter": "발신자 필터", + "senderFilterHelp": "발신자 주소 기준 필터입니다. 포함 패턴에 일치하면 허용되고, 제외 패턴에 일치하면 차단됩니다.", + "sizeLimit": "용량 제한", + "sizeLimitDesc": "이 크기를 초과하는 이메일은 보관 시 건너뜁니다. 제한하지 않으려면 비워 두세요.", + "skipLargerThan": "다음 용량보다 큰 이메일 건너뛰기:", + "spamHeaders": "스팸 헤더", + "spamHeadersHelp": "이러한 헤더 중 하나라도 'yes' 또는 'true'로 설정된 이메일은 건너뜁니다. 자주 사용되는 스팸 감지 헤더가 아래에 제안되어 있습니다.", + "subjectFilter": "제목 필터", + "subjectFilterHelp": "이메일 제목 기준 필터입니다. 발신자 필터링과 동일한 방식으로 작동합니다.", + "suggestions": "추천 항목 (클릭하여 추가):" + }, "fixed": "고정", "folderSync": { "autoSelectDescendants": "하위 항목 자동 선택", @@ -284,9 +320,25 @@ "selectUnit": "단위 선택", "selectedMailboxes": "선택된 메일함", "serverConfiguration": "서버 구성 (IMAP)", + "settings": { + "download": "다운로드 설정", + "downloadDesc": "서버에서 이메일을 가져오는 시기와 방법을 설정합니다.", + "filters": "필터", + "filtersDesc": "보관할 이메일을 제어합니다. 필터링을 비활성화하면 모든 이메일이 저장됩니다.", + "general": "기본 정보", + "generalDesc": "기본 계정 정보 및 상태입니다.", + "newAccount": "새 계정", + "performance": "성능", + "schedule": "시간 계획", + "scope": "동기화 범위", + "server": "서버 설정", + "serverDesc": "IMAP 연결 설정 및 인증입니다." + }, "since": "이후", "sinceFixed": "특정 날짜 이후", + "sinceFixedDesc": "선택한 날짜 이후에 수신된 이메일만 다운로드하며, 그 이전 이메일은 무시합니다.", "sinceRelative": "최근 이메일만 다운로드", + "sinceRelativeDesc": "최근 기간(예: 지난 3개월)의 이메일만 다운로드합니다. 시작 날짜는 시간이 지남에 따라 자동으로 이동합니다.", "sinceRelativeValue": "최근 기간의 이메일 다운로드", "startDownload": "다운로드 시작", "state": "상태", diff --git a/web/src/locales/nl.json b/web/src/locales/nl.json index 6e2c196..c9fb6dd 100644 --- a/web/src/locales/nl.json +++ b/web/src/locales/nl.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "Weet je zeker dat je dit account wilt {{action}}?", "auth": "Authenticatie", "authType": "authenticatie_type", - "autoConfiguring": "Automatisch configureren...", + "autoConfiguring": "Automatisch configureren…", + "autoDiscover": "Serverinstellingen automatisch detecteren", "autoDownloadNewMailboxes": "Nieuwe mappen automatisch importeren", "autoDownloadNewMailboxesDescription": "Voeg automatisch nieuw ontdekte mappen toe aan de downloadlijst.", "beforeRelative": "Download alleen oude e-mails", + "beforeRelativeDesc": "Download alleen e-mails die ouder zijn dan de opgegeven periode. De limietdatum verschuift automatisch – handig voor het geleidelijk archiveren van oude e-mails.", "beforeRelativeValue": "Download e-mails van {{value}} {{unit}} geleden", "cancelDownload": "Download annuleren", "cancelFailed": "Downloadtaak annuleren mislukt", @@ -124,9 +126,13 @@ "cronHour": "Uur", "cronMinute": "Minuut", "cronMonday": "Maandag", + "cronMonthAlignNote": "Opmerking: Als de maand korter is, wordt dit op de laatste dag van de maand uitgevoerd.", "cronMonthly": "Maandelijks", "cronSaturday": "Zaterdag", "cronSimple": "Simpele expressie", + "cronSummaryDaily": "Elke dag om {{hour}}:{{minute}}", + "cronSummaryMonthly": "Op dag {{day}} van elke maand om {{hour}}:{{minute}}", + "cronSummaryWeekly": "Elke {{day}} om {{hour}}:{{minute}}", "cronSunday": "Zondag", "cronThursday": "Donderdag", "cronTimezoneNote": "Alle tijden zijn server-lokale tijd.", @@ -145,6 +151,7 @@ "disableAccount": "Account uitschakelen", "disabled": "Uitgeschakeld", "downloadAll": "Download alle e-mails", + "downloadAllDesc": "Alle e-mails in elke geselecteerde mailbox worden gedownload. Best voor volledige archivering.", "downloadBatchSize": "Download batchgrootte", "downloadBatchSizeDescription": "Aantal berichten opgehaald per IMAP-verzoek", "downloadCancelled": "Taak geannuleerd", @@ -176,6 +183,35 @@ "enterValue": "Voer een waarde in", "everyMinutes": "elke {{minutes}} minuten", "field": "Veld", + "filters": { + "addHeader": "Header toevoegen", + "addPattern": "Patroon toevoegen", + "enableFiltering": "Inhoudsfiltering inschakelen", + "enableFilteringDesc": "Indien ingeschakeld, worden alleen e-mails die aan de onderstaande regels voldoen gearchiveerd. Indien uitgeschakeld, worden alle e-mails gearchiveerd.", + "exclude": "Exclusief", + "include": "Inclusief", + "matchType": { + "contains": "Bevat", + "endsWith": "Eindigt met", + "isExactly": "Is exact", + "regex": "Reguliere expressie (Regex)", + "startsWith": "Begint met" + }, + "noExcludePatterns": "Geen exclusiepatronen — niets is uitgesloten.", + "noIncludePatterns": "Geen inclusiepatronen — alles is inbegrepen.", + "noLimit": "Geen limiet", + "noSpamHeaders": "Geen spamheaders geconfigureerd.", + "senderFilter": "Afzenderfilter", + "senderFilterHelp": "Filter e-mails op afzender. Inclusiepatronen laten afzenders door, exclusiepatronen weigeren overeenkomende afzenders.", + "sizeLimit": "Groottebeperking", + "sizeLimitDesc": "E-mails die groter zijn dan deze omvang worden overgeslagen tijdens het archiveren. Laat leeg voor geen limiet.", + "skipLargerThan": "E-mails overslaan die groter zijn dan", + "spamHeaders": "Spamheaders", + "spamHeadersHelp": "E-mails waarbij een van deze headers is ingesteld na 'yes' of 'true' worden overgeslagen. Algemene spamheaders worden hieronder gesuggereerd.", + "subjectFilter": "Onderwerpfilter", + "subjectFilterHelp": "Filter e-mails op basis van de onderwerpregel. Werkt op dezelfde manier als afzenderfiltering.", + "suggestions": "Suggesties (klik om toe te voegen):" + }, "fixed": "Vast", "folderSync": { "autoSelectDescendants": "Automatisch onderliggende items selecteren", @@ -284,9 +320,25 @@ "selectUnit": "Selecteer een eenheid", "selectedMailboxes": "Geselecteerde mailboxen", "serverConfiguration": "Serverconfiguratie (IMAP)", + "settings": { + "download": "Downloaden", + "downloadDesc": "Configureren wanneer en hoe e-mails van de server worden opgehaald.", + "filters": "Filters", + "filtersDesc": "Bepaal welke e-mails worden gearchiveerd. Als filtering is uitgeschakeld, worden alle e-mails opgeslagen.", + "general": "Algemeen", + "generalDesc": "Basisaccountinformatie en status.", + "newAccount": "Nieuw account", + "performance": "Prestaties", + "schedule": "Tijdschema", + "scope": "Bereik", + "server": "Server", + "serverDesc": "IMAP-verbindinginstellingen en authenticatie." + }, "since": "sinds", "sinceFixed": "Sinds een specifieke datum", + "sinceFixedDesc": "Download alleen e-mails ontvangen na de geselecteerde datum. E-mails van vóór die datum worden genegeerd.", "sinceRelative": "Download alleen recente e-mails", + "sinceRelativeDesc": "Download alleen e-mails uit de afgelopen periode (bijv. laatste 3 maanden). De startdatum verschuift automatisch mee.", "sinceRelativeValue": "Download e-mails van de laatste", "startDownload": "Download starten", "state": "Status", diff --git a/web/src/locales/no.json b/web/src/locales/no.json index f865256..09dbd90 100644 --- a/web/src/locales/no.json +++ b/web/src/locales/no.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "Er du sikker på at du vil {{action}} denne kontoen?", "auth": "Autentisering", "authType": "autentiseringstype", - "autoConfiguring": "Konfigurerer automatisk...", + "autoConfiguring": "Konfigurerer automatisk…", + "autoDiscover": "Finn serverinnstillinger automatisk", "autoDownloadNewMailboxes": "Legg til nye mapper automatisk", "autoDownloadNewMailboxesDescription": "Legg automatisk til nye mapper i nedlastingslisten.", "beforeRelative": "Last ned kun gamle e-poster", + "beforeRelativeDesc": "Last bare ned e-poster som er eldre enn den angitte perioden. Skjæringsdatoen flyttes automatisk – nyttig for gradvis arkivering.", "beforeRelativeValue": "Last ned e-poster fra {{value}} {{unit}} siden", "cancelDownload": "Avbryt nedlasting", "cancelFailed": "Kunne ikke avbryte nedlastingsoppgave", @@ -124,9 +126,13 @@ "cronHour": "Time", "cronMinute": "Minutt", "cronMonday": "Mandag", + "cronMonthAlignNote": "Merk: Hvis måneden er kortere, vil den bli utført på månedens siste dag.", "cronMonthly": "Månedlig", "cronSaturday": "Lørdag", "cronSimple": "Enkelt uttrykk", + "cronSummaryDaily": "Hver dag kl. {{hour}}:{{minute}}", + "cronSummaryMonthly": "Den {{day}}. i hver måned kl. {{hour}}:{{minute}}", + "cronSummaryWeekly": "Hver {{day}} kl. {{hour}}:{{minute}}", "cronSunday": "Søndag", "cronThursday": "Torsdag", "cronTimezoneNote": "Alle klokkeslett er serverens lokaltid.", @@ -145,6 +151,7 @@ "disableAccount": "Deaktiver konto", "disabled": "Deaktivert", "downloadAll": "Last ned alle e-poster", + "downloadAllDesc": "Alle e-poster i hver valgte postkasse vil bli lastet ned. Best for komplett arkivering.", "downloadBatchSize": "Nedlastingsbatchstørrelse", "downloadBatchSizeDescription": "Antall meldinger hentet per IMAP-forespørsel", "downloadCancelled": "Oppgave avbrutt", @@ -176,6 +183,35 @@ "enterValue": "Vennligst skriv inn en verdi", "everyMinutes": "hvert {{minutes}} minutt", "field": "Felt", + "filters": { + "addHeader": "Legg til header", + "addPattern": "Legg til mønster", + "enableFiltering": "Aktiver innholdsfiltrering", + "enableFilteringDesc": "Når aktivert, vil bare e-poster som samsvarer med reglene nedenfor bli arkivert. Når deaktivert, arkiveres alle e-poster.", + "exclude": "Ekskluder", + "include": "Inkluder", + "matchType": { + "contains": "Inneholder", + "endsWith": "Ender med", + "isExactly": "Er nøyaktig", + "regex": "Regulært uttrykk (Regex)", + "startsWith": "Starter med" + }, + "noExcludePatterns": "Ingen ekskluderingsmønstre – ingenting er ekskludert.", + "noIncludePatterns": "Ingen inkluderingsmønstre – alt er inkludert.", + "noLimit": "Ingen grense", + "noSpamHeaders": "Ingen spam-headere er konfigurert.", + "senderFilter": "Afsenderfilter", + "senderFilterHelp": "Filtrer e-poster basert på avsenderadresse. Inkluderingsmønstre tillater avsendere, ekskluderingsmønstre avviser dem.", + "sizeLimit": "Størrelsesbegrensning", + "sizeLimitDesc": "E-poster større enn denne størrelsen vil bli hoppet over under arkivering. La stå tom for ingen grense.", + "skipLargerThan": "Hopp over e-poster større enn", + "spamHeaders": "Spam-headere", + "spamHeadersHelp": "E-poster med en av disse headerne satt til 'yes' eller 'true' vil bli hoppet over. Vanlige spam-headere er foreslått nedenfor.", + "subjectFilter": "Emnefilter", + "subjectFilterHelp": "Filtrer e-poster basert på emnelinjen. Fungerer på samme måte som avsenderfiltrering.", + "suggestions": "Forslag (klikk for å legge til):" + }, "fixed": "Fast", "folderSync": { "autoSelectDescendants": "Velg etterkommere automatisk", @@ -284,9 +320,25 @@ "selectUnit": "Velg en enhet", "selectedMailboxes": "Valgte postbokser", "serverConfiguration": "Serverkonfigurasjon (IMAP)", + "settings": { + "download": "Nedlasting", + "downloadDesc": "Konfigurer når og hvordan e-poster hentes fra serveren.", + "filters": "Filtre", + "filtersDesc": "Styr hvilke e-poster som arkiveres. Når filtrering er deaktivert, lagres alle e-poster.", + "general": "Generelt", + "generalDesc": "Grunnleggende kontoinformasjon og status.", + "newAccount": "Ny konto", + "performance": "Ytelse", + "schedule": "Tidsplan", + "scope": "Omfang", + "server": "Server", + "serverDesc": "IMAP-tilkoblingsinnstillinger og autentisering." + }, "since": "siden", "sinceFixed": "Siden spesifikk dato", + "sinceFixedDesc": "Last bare ned e-poster mottatt etter valgt dato. E-poster før denne datoen ignoreres.", "sinceRelative": "Last ned kun nylige e-poster", + "sinceRelativeDesc": "Last bare ned e-poster fra den siste perioden (f.eks. siste 3 måneder). Startdatoen flyttes automatisk fremover.", "sinceRelativeValue": "Last ned e-poster fra de siste", "startDownload": "Start nedlasting", "state": "Tilstand", diff --git a/web/src/locales/pl.json b/web/src/locales/pl.json index cb8a90a..93e1b9c 100644 --- a/web/src/locales/pl.json +++ b/web/src/locales/pl.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "Czy na pewno chcesz {{action}} dla tego konta?", "auth": "Auth", "authType": "auth_type", - "autoConfiguring": "Auto konfiguracja...", + "autoConfiguring": "Automatyczna konfiguracja…", + "autoDiscover": "Automatyczne wykrywanie ustawień serwera", "autoDownloadNewMailboxes": "Automatycznie dodawaj nowe foldery", "autoDownloadNewMailboxesDescription": "Automatycznie dodawaj nowo wykryte foldery do listy pobierania.", "beforeRelative": "Pobierz tylko stare e-maile", + "beforeRelativeDesc": "Pobieraj tylko wiadomości starsze niż określony czas. Data graniczna automatycznie przesuwa się w czasie – przydatne do stopniowej archiwizacji starych wiadomości.", "beforeRelativeValue": "Pobierz e-maile sprzed {{value}} {{unit}}", "cancelDownload": "Anuluj pobieranie", "cancelFailed": "Nie udało się anulować zadania pobierania", @@ -124,9 +126,13 @@ "cronHour": "Godzina", "cronMinute": "Minuta", "cronMonday": "Poniedziałek", + "cronMonthAlignNote": "Uwaga: Jeśli miesiąc jest krótszy, wykonanie nastąpi w ostatnim dniu miesiąca.", "cronMonthly": "Co miesiąc", "cronSaturday": "Sobota", "cronSimple": "Proste wyrażenie", + "cronSummaryDaily": "Codziennie o {{hour}}:{{minute}}", + "cronSummaryMonthly": "Każdego {{day}}. dnia miesiąca o {{hour}}:{{minute}}", + "cronSummaryWeekly": "W każdy(ą) {{day}} o {{hour}}:{{minute}}", "cronSunday": "Niedziela", "cronThursday": "Czwartek", "cronTimezoneNote": "Czas według lokalnej strefy serwera.", @@ -145,6 +151,7 @@ "disableAccount": "Wyłącz konto", "disabled": "Wyłączone", "downloadAll": "Pobierz wszystkie e-maile", + "downloadAllDesc": "Wszystkie wiadomości w wybranych skrzynkach zostaną pobrane. Najlepsze do kompletnej archiwizacji.", "downloadBatchSize": "Rozmiar partii pobierania", "downloadBatchSizeDescription": "Liczba wiadomości pobieranych na żądanie IMAP", "downloadCancelled": "Zadanie anulowane", @@ -176,6 +183,35 @@ "enterValue": "Wpisz wartość", "everyMinutes": "co {{minutes}} minut", "field": "Pole", + "filters": { + "addHeader": "Dodaj nagłówek", + "addPattern": "Dodaj regułę", + "enableFiltering": "Włącz filtrowanie zawartości", + "enableFilteringDesc": "Po włączeniu archiwizowane będą tylko wiadomości spełniające poniższe reguły. Po wyłączeniu archiwizowane są wszystkie wiadomości.", + "exclude": "Wyklucz", + "include": "Uwzględnij", + "matchType": { + "contains": "Zawiera", + "endsWith": "Kończy się na", + "isExactly": "Równa się", + "regex": "Wyrażenie regularne (Regex)", + "startsWith": "Zaczyna się od" + }, + "noExcludePatterns": "Brak reguł wykluczania — nic nie zostanie wykluczone.", + "noIncludePatterns": "Brak reguł uwzględniania — wszystko zostanie uwzględnione.", + "noLimit": "Bez limitu", + "noSpamHeaders": "Nie skonfigurowano nagłówków spamu.", + "senderFilter": "Filtr nadawcy", + "senderFilterHelp": "Filtruj wiadomości według adresu nadawcy. Reguły uwzględniania przepuszczają nadawców, reguły wykluczania ich odrzucają.", + "sizeLimit": "Limit rozmiaru", + "sizeLimitDesc": "Wiadomości przekraczające ten rozmiar zostaną pominięte podczas archiwizacji. Pozostaw puste, aby nie nakładać limitu.", + "skipLargerThan": "Pomiń wiadomości większe niż", + "spamHeaders": "Nagłówki spamu", + "spamHeadersHelp": "Wiadomości z dowolnym z tych nagłówków ustawionym na 'yes' lub 'true' zostaną pominięte. Poniżej sugerowane są popularne nagłówki spamu.", + "subjectFilter": "Filtr tematu", + "subjectFilterHelp": "Filtruj wiadomości według tematu. Działa w ten sam sposób, co filtrowanie nadawców.", + "suggestions": "Sugerowane (kliknij, aby dodać):" + }, "fixed": "Dokładnie", "folderSync": { "autoSelectDescendants": "Automatyczny wybór dzieci (podrzędnych)", @@ -284,9 +320,25 @@ "selectUnit": "Zaznacz jednostki", "selectedMailboxes": "Wybrane skrzynki", "serverConfiguration": "Konfiguracja serwera (IMAP)", + "settings": { + "download": "Pobieranie", + "downloadDesc": "Konfiguruj, kiedy i jak wiadomości e-mail są pobierane z serwera.", + "filters": "Filtry", + "filtersDesc": "Kontroluj, które wiadomości są archiwizowane. Gdy filtrowanie jest wyłączone, zapisywane są wszystkie wiadomości.", + "general": "Ogólne", + "generalDesc": "Podstawowe informacje o koncie i jego status.", + "newAccount": "Nowe konto", + "performance": "Wydajność", + "schedule": "Harmonogram", + "scope": "Zakres", + "server": "Serwer", + "serverDesc": "Ustawienia połączenia IMAP i uwierzytelnianie." + }, "since": "od", "sinceFixed": "Od konkretnej daty", + "sinceFixedDesc": "Pobieraj tylko wiadomości odebrane po wybranej dacie. Wiadomości sprzed tej daty będą ignorowane.", "sinceRelative": "Pobierz tylko ostatnie e-maile", + "sinceRelativeDesc": "Pobieraj tylko wiadomości z ostatniego okresu (np. ostatnie 3 miesiące). Data początkowa automatycznie przesuwa się w czasie.", "sinceRelativeValue": "Pobierz e-maile z ostatnich", "startDownload": "Uruchom pobieranie", "state": "Status", diff --git a/web/src/locales/pt.json b/web/src/locales/pt.json index c18de84..af17c7a 100644 --- a/web/src/locales/pt.json +++ b/web/src/locales/pt.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "Tem certeza de que deseja {{action}} esta conta?", "auth": "Autenticação", "authType": "Tipo de Autenticação", - "autoConfiguring": "Configurando Automaticamente...", + "autoConfiguring": "Configurando automaticamente…", + "autoDiscover": "Autodetectar configurações do servidor", "autoDownloadNewMailboxes": "Adicionar automaticamente novas pastas", "autoDownloadNewMailboxesDescription": "Adicionar automaticamente novas pastas à lista de download.", "beforeRelative": "Baixar apenas e-mails antigos", + "beforeRelativeDesc": "Baixar apenas e-mails anteriores ao período especificado. A data limite avança automaticamente com o tempo — útil para arquivar e-mails antigos gradualmente.", "beforeRelativeValue": "Baixar e-mails de {{value}} {{unit}} atrás", "cancelDownload": "Cancelar download", "cancelFailed": "Falha ao cancelar tarefa de download", @@ -124,9 +126,13 @@ "cronHour": "Hora", "cronMinute": "Minuto", "cronMonday": "Segunda-feira", + "cronMonthAlignNote": "Nota: Se o mês for mais curto, a execução ocorrerá no último dia do mês.", "cronMonthly": "Mensalmente", "cronSaturday": "Sábado", "cronSimple": "Expressão simples", + "cronSummaryDaily": "Todos os dias às {{hour}}:{{minute}}", + "cronSummaryMonthly": "No dia {{day}} de cada mês às {{hour}}:{{minute}}", + "cronSummaryWeekly": "Toda(o) {{day}} às {{hour}}:{{minute}}", "cronSunday": "Domingo", "cronThursday": "Quinta-feira", "cronTimezoneNote": "Horários no fuso horário do servidor.", @@ -145,6 +151,7 @@ "disableAccount": "Desativar conta", "disabled": "Desativado", "downloadAll": "Baixar todos os e-mails", + "downloadAllDesc": "Todos os e-mails de cada caixa de correio selecionada serão baixados. Ideal para arquivamento completo.", "downloadBatchSize": "Tamanho do lote de download", "downloadBatchSizeDescription": "Número de mensagens recuperadas por solicitação IMAP", "downloadCancelled": "Tarefa cancelada", @@ -176,6 +183,35 @@ "enterValue": "Insira o Valor", "everyMinutes": "A cada {{minutes}} minutos", "field": "Campo", + "filters": { + "addHeader": "Adicionar cabeçalho", + "addPattern": "Adicionar padrão", + "enableFiltering": "Ativar filtragem de conteúdo", + "enableFilteringDesc": "Quando ativado, apenas os e-mails que correspondem às regras abaixo serão arquivados. Quando desativado, todos os e-mails são arquivados.", + "exclude": "Excluir", + "include": "Incluir", + "matchType": { + "contains": "Contém", + "endsWith": "Termina com", + "isExactly": "É exatamente", + "regex": "Expressão regular (Regex)", + "startsWith": "Começa com" + }, + "noExcludePatterns": "Sem padrões de exclusão — nada está excluído.", + "noIncludePatterns": "Sem padrões de inclusão — tudo está incluído.", + "noLimit": "Sem limite", + "noSpamHeaders": "Nenhum cabeçalho de spam configurado.", + "senderFilter": "Filtro de remetente", + "senderFilterHelp": "Filtre e-mails pelo endereço do remetente. Padrões de inclusão aprovam os remetentes; padrões de exclusão rejeitam os correspondentes.", + "sizeLimit": "Limite de tamanho", + "sizeLimitDesc": "E-mails maiores que este tamanho serão ignorados durante o arquivamento. Deixe em branco para não limitar.", + "skipLargerThan": "Ignorar e-mails maiores que", + "spamHeaders": "Cabeçalhos de spam", + "spamHeadersHelp": "E-mails com qualquer um desses cabeçalhos definidos como 'yes' ou 'true' serão ignorados. Cabeçalhos comuns de detecção de spam são sugeridos abaixo.", + "subjectFilter": "Filtro de assunto", + "subjectFilterHelp": "Filtre e-mails com base na linha de assunto. Funciona da mesma forma que a filtragem de remetentes.", + "suggestions": "Sugestões (clique para adicionar):" + }, "fixed": "Fixo", "folderSync": { "autoSelectDescendants": "Selecionar automaticamente os descendentes", @@ -284,9 +320,25 @@ "selectUnit": "Selecionar Unidade", "selectedMailboxes": "Caixas de correio selecionadas", "serverConfiguration": "Configuração do Servidor (IMAP)", + "settings": { + "download": "Download", + "downloadDesc": "Configure quando e como os e-mails são buscados no servidor.", + "filters": "Filtros", + "filtersDesc": "Controle quais e-mails serão arquivados. Quando a filtragem está desativada, todos os e-mails são salvos.", + "general": "Geral", + "generalDesc": "Informações básicas da conta e status.", + "newAccount": "Nova conta", + "performance": "Desempenho", + "schedule": "Cronograma", + "scope": "Escopo", + "server": "Servidor", + "serverDesc": "Configurações de conexão IMAP e autenticação." + }, "since": "Desde", "sinceFixed": "Desde uma data específica", + "sinceFixedDesc": "Baixar apenas e-mails recebidos após a data selecionada. E-mails anteriores serão ignorados.", "sinceRelative": "Baixar apenas e-mails recentes", + "sinceRelativeDesc": "Baixar apenas e-mails do período recente (ex: últimos 3 meses). A data de início avança automaticamente com o tempo.", "sinceRelativeValue": "Baixar e-mails dos últimos", "startDownload": "Iniciar download", "state": "Estado", diff --git a/web/src/locales/ru.json b/web/src/locales/ru.json index 96bebff..292df30 100644 --- a/web/src/locales/ru.json +++ b/web/src/locales/ru.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "Вы уверены, что хотите {{action}} этот аккаунт?", "auth": "Авторизация", "authType": "тип_авторизации", - "autoConfiguring": "Автонастройка...", + "autoConfiguring": "Автоматическая настройка…", + "autoDiscover": "Автоопределение настроек сервера", "autoDownloadNewMailboxes": "Автодобавление новых папок", "autoDownloadNewMailboxesDescription": "Автоматически добавлять новые папки в список загрузки.", "beforeRelative": "Скачать только старые письма", + "beforeRelativeDesc": "Скачивать только письма старше указанного периода. Дата отсечки автоматически сдвигается со временем — полезно для постепенного архивирования старых писем.", "beforeRelativeValue": "Скачать письма за {{value}} {{unit}} назад", "cancelDownload": "Отменить загрузку", "cancelFailed": "Не удалось отменить задачу загрузки", @@ -124,9 +126,13 @@ "cronHour": "Час", "cronMinute": "Минута", "cronMonday": "Понедельник", + "cronMonthAlignNote": "Примечание: Если в месяце меньше дней, выполнение произойдет в последний день месяца.", "cronMonthly": "Ежемесячно", "cronSaturday": "Суббота", "cronSimple": "Простое выражение", + "cronSummaryDaily": "Каждый день в {{hour}}:{{minute}}", + "cronSummaryMonthly": "{{day}}-го числа каждого месяца в {{hour}}:{{minute}}", + "cronSummaryWeekly": "Каждую(ый) {{day}} в {{hour}}:{{minute}}", "cronSunday": "Воскресенье", "cronThursday": "Четверг", "cronTimezoneNote": "Время по местному часовому поясу сервера.", @@ -145,6 +151,7 @@ "disableAccount": "Отключить аккаунт", "disabled": "Отключено", "downloadAll": "Скачать все письма", + "downloadAllDesc": "Все письма в выбранных ящиках будут скачаны от первых до последних. Идеально для полного архивирования.", "downloadBatchSize": "Размер пакета загрузки", "downloadBatchSizeDescription": "Количество сообщений, получаемых за один IMAP-запрос", "downloadCancelled": "Задача отменена", @@ -176,6 +183,35 @@ "enterValue": "Пожалуйста, введите значение", "everyMinutes": "каждые {{minutes}} мин.", "field": "Поле", + "filters": { + "addHeader": "Добавить заголовок", + "addPattern": "Добавить шаблон", + "enableFiltering": "Включить фильтрацию контента", + "enableFilteringDesc": "Если включено, будут архивироваться только письма, соответствующие правилам ниже. Если отключено — архивируются все письма.", + "exclude": "Исключить", + "include": "Включить", + "matchType": { + "contains": "Содержит", + "endsWith": "Заканчивается на", + "isExactly": "Точное совпадение", + "regex": "Регулярное выражение (Regex)", + "startsWith": "Начинается с" + }, + "noExcludePatterns": "Нет шаблонов исключения — ничто не исключено.", + "noIncludePatterns": "Нет шаблонов включения — включено всё.", + "noLimit": "Без ограничений", + "noSpamHeaders": "Заголовки спама не настроены.", + "senderFilter": "Фильтр отправителей", + "senderFilterHelp": "Фильтрация по адресу отправителя. Шаблоны включения пропускают отправителей, а шаблоны исключения — отклоняют.", + "sizeLimit": "Лимит по размеру", + "sizeLimitDesc": "Письма крупнее этого размера будут пропущены при архивации. Оставьте пустым, чтобы не ограничивать размер.", + "skipLargerThan": "Пропускать письма крупнее", + "spamHeaders": "Заголовки спама", + "spamHeadersHelp": "Письма, у которых любой из этих заголовков имеет значение 'yes' или 'true', будут пропущены. Ниже приведены популярные заголовки для обнаружения спама.", + "subjectFilter": "Фильтр тем", + "subjectFilterHelp": "Фильтрация по теме письма. Работает так же, как и фильтрация отправителей.", + "suggestions": "Рекомендации (нажмите, чтобы добавить):" + }, "fixed": "Фиксированная", "folderSync": { "autoSelectDescendants": "Автоматически выбирать дочерние элементы", @@ -284,9 +320,25 @@ "selectUnit": "Выберите единицу", "selectedMailboxes": "Выбранные почтовые ящики", "serverConfiguration": "Конфигурация сервера (IMAP)", + "settings": { + "download": "Скачивание", + "downloadDesc": "Настройка времени и способа получения писем с сервера.", + "filters": "Фильтры", + "filtersDesc": "Управляйте тем, какие письма архивировать. Если фильтрация отключена, будут сохраняться все письма.", + "general": "Общие", + "generalDesc": "Основная информация об аккаунте и его статус.", + "newAccount": "Новый аккаунт", + "performance": "Производительность", + "schedule": "Расписание", + "scope": "Период синхронизации", + "server": "Сервер", + "serverDesc": "Настройки подключения IMAP и аутентификация." + }, "since": "с", "sinceFixed": "С определенной даты", + "sinceFixedDesc": "Скачивать только письма, полученные после указанной даты. Более ранние письма будут проигнорированы.", "sinceRelative": "Скачать только недавние письма", + "sinceRelativeDesc": "Скачивать письма только за последний период (напр., за 3 месяца). Дата начала автоматически сдвигается со временем.", "sinceRelativeValue": "Скачать письма за последние", "startDownload": "Запустить загрузку", "state": "Состояние", diff --git a/web/src/locales/sv.json b/web/src/locales/sv.json index c0975d6..4bd022f 100644 --- a/web/src/locales/sv.json +++ b/web/src/locales/sv.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "Är du säker på att du vill {{action}} detta konto?", "auth": "Auth", "authType": "auth_typ", - "autoConfiguring": "Konfigurerar automatiskt...", + "autoConfiguring": "Konfigurerar automatiskt…", + "autoDiscover": "Hitta serverinställningar automatiskt", "autoDownloadNewMailboxes": "Lägg till nya mappar automatiskt", "autoDownloadNewMailboxesDescription": "Lägg automatiskt till nya mappar i hämtningslistan.", "beforeRelative": "Ladda ner endast gamla e-postmeddelanden", + "beforeRelativeDesc": "Ladda endast ner e-postmeddelanden som är äldre än den angivna perioden. Brytdatumet flyttas automatiskt – användbart för att gradvis arkivera gamla e-postmeddelanden.", "beforeRelativeValue": "Ladda ner e-post från {{value}} {{unit}} sedan", "cancelDownload": "Avbryt hämtning", "cancelFailed": "Misslyckades med att avbryta hämtningsuppgift", @@ -124,9 +126,13 @@ "cronHour": "Timme", "cronMinute": "Minut", "cronMonday": "Måndag", + "cronMonthAlignNote": "Obs: Om månaden är kortare kommer det att utföras på månadens sista dag.", "cronMonthly": "Månadsvis", "cronSaturday": "Lördag", "cronSimple": "Enkelt uttryck", + "cronSummaryDaily": "Varje dag kl. {{hour}}:{{minute}}", + "cronSummaryMonthly": "Den {{day}}:e i varje månad kl. {{hour}}:{{minute}}", + "cronSummaryWeekly": "Varje {{day}} kl. {{hour}}:{{minute}}", "cronSunday": "Söndag", "cronThursday": "Torsdag", "cronTimezoneNote": "Alla tider visas i serverns lokaltid.", @@ -145,6 +151,7 @@ "disableAccount": "Inaktivera konto", "disabled": "Inaktiverad", "downloadAll": "Ladda ner alla e-postmeddelanden", + "downloadAllDesc": "Alla e-postmeddelanden i varje vald brevlåda kommer att laddas ner. Bäst för komplett arkivering.", "downloadBatchSize": "Batchstorlek för nedladdning", "downloadBatchSizeDescription": "Antal meddelanden som hämtas per IMAP-begäran", "downloadCancelled": "Uppgiften avbruten", @@ -176,6 +183,35 @@ "enterValue": "Ange ett värde", "everyMinutes": "varje {{minutes}} minut", "field": "Fält", + "filters": { + "addHeader": "Lägg till rubrik", + "addPattern": "Lägg till mönster", + "enableFiltering": "Aktivera innehållsfiltrering", + "enableFilteringDesc": "När den er aktiverad kommer endast e-postmeddelanden som matchar reglerna nedan att arkiveras. När den är inaktiverad arkiveras alla e-postmeddelanden.", + "exclude": "Exkludera", + "include": "Inkludera", + "matchType": { + "contains": "Innehåller", + "endsWith": "Slutar med", + "isExactly": "Är exakt", + "regex": "Reguljärt uttryck (Regex)", + "startsWith": "Börjar med" + }, + "noExcludePatterns": "Inga exkluderingsmönster – inget exkluderas.", + "noIncludePatterns": "Inga inkluderingsmönster – allt inkluderas.", + "noLimit": "Ingen gräns", + "noSpamHeaders": "Inga spamrubriker har konfigurerats.", + "senderFilter": "Afsendarefilter", + "senderFilterHelp": "Filtrera e-post baserat på avsändaradress. Inkluderingsmönster godkänner avsändare, exkluderingsmönster avvisar dem.", + "sizeLimit": "Storleksgräns", + "sizeLimitDesc": "E-postmeddelanden som är större än denna storlek kommer att hoppas över vid arkivering. Lämna tomt för ingen gräns.", + "skipLargerThan": "Hoppa över e-postmeddelanden större än", + "spamHeaders": "Spamrubriker", + "spamHeadersHelp": "E-postmeddelanden där någon av dessa rubriker är satt till 'yes' eller 'true' kommer att hoppas över. Vanliga spamrubriker föreslås nedan.", + "subjectFilter": "Ämnesfilter", + "subjectFilterHelp": "Filtrera e-post baserat på ämnesraden. Fungerar på samma sätt som avsändarfiltrering.", + "suggestions": "Förslag (klicka för att lägga till):" + }, "fixed": "Fast", "folderSync": { "autoSelectDescendants": "Välj underordnade automatiskt", @@ -284,9 +320,25 @@ "selectUnit": "Välj en enhet", "selectedMailboxes": "Valda brevlådor", "serverConfiguration": "Serverkonfiguration (IMAP)", + "settings": { + "download": "Ladda ner", + "downloadDesc": "Konfigurera när och hur e-postmeddelanden hämtas från serveren.", + "filters": "Filter", + "filtersDesc": "Styr vilka e-postmeddelanden som arkiveras. När filtrering är inaktiverad sparas alla e-postmeddelanden.", + "general": "Allmänt", + "generalDesc": "Grundläggande kontoinformation och status.", + "newAccount": "Nytt konto", + "performance": "Prestanda", + "schedule": "Tidsplan", + "scope": "Omfång", + "server": "Server", + "serverDesc": "IMAP-anslutningsinställningar och autentisering." + }, "since": "sedan", "sinceFixed": "Sedan specifikt datum", + "sinceFixedDesc": "Ladda endast ner e-postmeddelanden som tagits emot efter det valda datumet. Tidigare e-postmeddelanden ignoreras.", "sinceRelative": "Ladda ner endast senaste e-postmeddelanden", + "sinceRelativeDesc": "Ladda endast ner e-postmeddelanden från den senaste perioden (t.ex. senaste 3 månaderna). Startdatumet flyttas automatiskt framåt.", "sinceRelativeValue": "Ladda ner e-post från de senaste", "startDownload": "Starta hämtning", "state": "Tillstånd", diff --git a/web/src/locales/zh-tw.json b/web/src/locales/zh-tw.json index c733ac5..19c5375 100644 --- a/web/src/locales/zh-tw.json +++ b/web/src/locales/zh-tw.json @@ -99,10 +99,12 @@ "areYouSureYouWantTo": "你確定要{{action}}此帳戶嗎?", "auth": "驗證", "authType": "驗證類型", - "autoConfiguring": "正在自動設定...", + "autoConfiguring": "正在自動設定…", + "autoDiscover": "自動偵測伺服器設定", "autoDownloadNewMailboxes": "自動添加新郵件夾", "autoDownloadNewMailboxesDescription": "自動將新發現的郵件夾添加到下載列表中。", "beforeRelative": "僅下載舊郵件", + "beforeRelativeDesc": "僅下載早於指定時間段的舊郵件。截止日期會隨時間自動向後推移,適合逐步封存舊郵件並跳過新郵件。", "beforeRelativeValue": "下載 {{value}} {{unit}} 之前的郵件", "cancelDownload": "取消下載", "cancelFailed": "取消下載任務失敗", @@ -124,9 +126,13 @@ "cronHour": "小時", "cronMinute": "分鐘", "cronMonday": "星期一", + "cronMonthAlignNote": "注意:若當月天數不足,將在該月的最後一天執行。", "cronMonthly": "每月", "cronSaturday": "星期六", "cronSimple": "简易表達式", + "cronSummaryDaily": "每天 {{hour}}:{{minute}}", + "cronSummaryMonthly": "每月 {{day}} 日 {{hour}}:{{minute}}", + "cronSummaryWeekly": "每周{{day}} {{hour}}:{{minute}}", "cronSunday": "星期日", "cronThursday": "星期四", "cronTimezoneNote": "所有時間均使用伺服器在地時區。", @@ -145,6 +151,7 @@ "disableAccount": "停用帳戶", "disabled": "已停用", "downloadAll": "下載所有郵件", + "downloadAllDesc": "將下載選取信箱中的所有郵件(從第一封到最新一封)。最適合完整封存。", "downloadBatchSize": "下載批量大小", "downloadBatchSizeDescription": "每個 IMAP 請求獲取的郵件數量", "downloadCancelled": "下載任務已取消", @@ -176,6 +183,35 @@ "enterValue": "輸入值", "everyMinutes": "每 {{minutes}} 分鐘", "field": "欄位", + "filters": { + "addHeader": "新增標頭", + "addPattern": "新增規則", + "enableFiltering": "启用內容篩選", + "enableFilteringDesc": "開啟後,僅封存符合下方規則的郵件;關閉後,將封存所有郵件。", + "exclude": "排除", + "include": "包含", + "matchType": { + "contains": "包含", + "endsWith": "結尾為", + "isExactly": "完全符合", + "regex": "正規表示式", + "startsWith": "開頭為" + }, + "noExcludePatterns": "無排除規則 — 無內容被排除。", + "noIncludePatterns": "無包含規則 — 所有內容均將被包含。", + "noLimit": "不限制", + "noSpamHeaders": "未設定垃圾郵件標頭。", + "senderFilter": "寄件者篩選", + "senderFilterHelp": "基於寄件者電子郵件地址進行篩選。符合「包含規則」的允許通過,符合「排除規則」的則予以拒絕。", + "sizeLimit": "大小限制", + "sizeLimitDesc": "超過此大小的郵件在封存時將被跳過。留空則表示不限制大小。", + "skipLargerThan": "跳過大小超過以下值的郵件", + "spamHeaders": "垃圾郵件標頭", + "spamHeadersHelp": "若郵件包含以下任一標頭且值為 'yes' 或 'true',將被自動跳過。下方列出了常用的垃圾郵件偵測標記。", + "subjectFilter": "主旨篩選", + "subjectFilterHelp": "基於郵件主旨進行篩選。運作原理與寄件者篩選相同。", + "suggestions": "▼ 建議標記(點擊新增):" + }, "fixed": "固定", "folderSync": { "autoSelectDescendants": "自動選取子項", @@ -284,9 +320,25 @@ "selectUnit": "選擇單位", "selectedMailboxes": "已選擇的郵件夾", "serverConfiguration": "伺服器設定 (IMAP)", + "settings": { + "download": "下載設定", + "downloadDesc": "設定從伺服器獲取與收取郵件的时间与方式。", + "filters": "篩選器", + "filtersDesc": "控制哪些郵件需要封存。關閉篩選時,將儲存所有郵件。", + "general": "基本資訊", + "generalDesc": "基本帳戶資訊与狀態。", + "newAccount": "建立新帳戶", + "performance": "效能", + "schedule": "時間排程", + "scope": "下載範圍", + "server": "伺服器設定", + "serverDesc": "IMAP 連線設定與驗證。" + }, "since": "自", "sinceFixed": "自特定日期起", + "sinceFixedDesc": "僅下載指定日期之後收到的郵件,此前的郵件將被忽略。", "sinceRelative": "僅下載最近郵件", + "sinceRelativeDesc": "僅下載最近一段時間(如過去 3 個月)的郵件。開始日期會隨時間推移自動向前滾動。", "sinceRelativeValue": "下載最近一段時期的郵件", "startDownload": "啟動下載", "state": "狀態", diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index fdcda37..b468399 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -98,11 +98,14 @@ "allMailSkipped": "已选择标准文件夹。已跳过'所有邮件'以避免重复。", "areYouSureYouWantTo": "你确定要{{action}}此账户吗?", "auth": "认证", + "authPassword": "密码", "authType": "认证类型", - "autoConfiguring": "自动配置中...", + "autoConfiguring": "正在自动配置…", + "autoDiscover": "自动检测服务器设置", "autoDownloadNewMailboxes": "自动添加新邮件夹", "autoDownloadNewMailboxesDescription": "自动将新发现的邮件夹添加到下载列表中。", "beforeRelative": "仅下载旧邮件", + "beforeRelativeDesc": "仅下载早于指定时间段的旧邮件。截止日期会随时间自动向后推移,适合逐步归档旧邮件并跳过新邮件。", "beforeRelativeValue": "下载 {{value}} {{unit}} 之前的邮件", "cancelDownload": "取消下载", "cancelFailed": "取消下载任务失败", @@ -114,19 +117,24 @@ "clickSaveWhenDone": "完成后请点击保存。", "continue": "继续", "createdAt": "创建时间", + "creating": "创建中...", "creationFailed": "创建失败,请稍后重试", "cronAdvanced": "高级表达式", "cronDaily": "每天", - "cronDayOfMonth": "星期", + "cronDayOfMonth": "几号", "cronDayOfWeek": "星期", "cronFrequency": "频率", "cronFriday": "星期五", "cronHour": "小时", "cronMinute": "分钟", "cronMonday": "星期一", + "cronMonthAlignNote": "注意:若当月天数不足,将在该月的最后一天执行。", "cronMonthly": "每月", "cronSaturday": "星期六", "cronSimple": "简易表达式", + "cronSummaryDaily": "每天 {{hour}}:{{minute}}", + "cronSummaryMonthly": "每月 {{day}} 日 {{hour}}:{{minute}}", + "cronSummaryWeekly": "每周{{day}} {{hour}}:{{minute}}", "cronSunday": "星期日", "cronThursday": "星期四", "cronTimezoneNote": "所有时间均使用服务器本地时区。", @@ -145,6 +153,7 @@ "disableAccount": "禁用账户", "disabled": "已禁用", "downloadAll": "下载所有邮件", + "downloadAllDesc": "将下载选中邮箱中的所有邮件(从第一封到最新一封)。最适合完整归档。", "downloadBatchSize": "下载批量大小", "downloadBatchSizeDescription": "每个 IMAP 请求获取的邮件数量", "downloadCancelled": "下载任务已取消", @@ -176,6 +185,35 @@ "enterValue": "请输入一个值", "everyMinutes": "每 {{minutes}} 分钟", "field": "字段", + "filters": { + "addHeader": "添加头字段", + "addPattern": "添加规则", + "enableFiltering": "启用内容过滤", + "enableFilteringDesc": "开启后,仅归档符合下方规则的邮件;关闭后,将归档所有邮件。", + "exclude": "排除", + "include": "包含", + "matchType": { + "contains": "包含", + "endsWith": "结尾为", + "isExactly": "完全匹配", + "regex": "正则表达式", + "startsWith": "开头为" + }, + "noExcludePatterns": "无排除规则 — 无内容被排除。", + "noIncludePatterns": "无包含规则 — 所有内容均将被包含。", + "noLimit": "不限制", + "noSpamHeaders": "未配置垃圾邮件头字段。", + "senderFilter": "发件人过滤", + "senderFilterHelp": "基于发件人邮箱地址进行过滤。符合“包含规则”的允许通过,符合“排除规则”的则予以拒绝。", + "sizeLimit": "大小限制", + "sizeLimitDesc": "超过此大小的邮件在归档时将被跳过。留空则表示不限制大小。", + "skipLargerThan": "跳过大小超过以下值的邮件", + "spamHeaders": "垃圾邮件头字段", + "spamHeadersHelp": "若邮件包含以下任一头字段且值为 'yes' 或 'true',将被自动跳过。下方列出了常用的垃圾邮件检测标志。", + "subjectFilter": "主题过滤", + "subjectFilterHelp": "基于邮件主题进行过滤。工作原理与发件人过滤相同。", + "suggestions": "推荐配置(点击添加):" + }, "fixed": "固定", "folderSync": { "autoSelectDescendants": "自动选择子项", @@ -284,9 +322,33 @@ "selectUnit": "选择单位", "selectedMailboxes": "已选择的邮件夹", "serverConfiguration": "服务器配置 (IMAP)", + "settings": { + "backToAccounts": "返回账户列表", + "download": "下载设置", + "downloadDesc": "配置从服务器获取和收取邮件的时间与方式。", + "filters": "过滤器", + "filtersDesc": "控制哪些邮件需要归档。关闭过滤时,将保存所有邮件。", + "general": "基本信息", + "generalDesc": "基本账户信息与状态。", + "loading": "加载账户中...", + "newAccount": "新建账户", + "performance": "性能", + "reset": "重置", + "save": "保存", + "saved": "已保存", + "savedDesc": "设置已成功保存。", + "saving": "保存中...", + "schedule": "时间计划", + "scope": "下载范围", + "server": "服务器设置", + "serverDesc": "IMAP 连接设置与身份验证。", + "settings": "设置" + }, "since": "自", "sinceFixed": "自特定日期起", + "sinceFixedDesc": "仅下载指定日期之后收到的邮件,此前的邮件将被忽略。", "sinceRelative": "仅下载最近邮件", + "sinceRelativeDesc": "仅下载最近一段时间(如过去 3 个月)的邮件。开始日期会随时间推移自动向后滚动。", "sinceRelativeValue": "下载最近一段时期的邮件", "startDownload": "启动下载", "state": "状态", diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 64f735d..55516b5 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -76,6 +76,12 @@ const AuthenticatedSettingsApiTokensLazyImport = createFileRoute( const AuthenticatedSettingsAccessLazyImport = createFileRoute( '/_authenticated/settings/access', )() +const AuthenticatedAccountsNewLazyImport = createFileRoute( + '/_authenticated/accounts/new', +)() +const AuthenticatedAccountsIdSettingsLazyImport = createFileRoute( + '/_authenticated/accounts/$id/settings', +)() // Create/Update Routes @@ -317,6 +323,26 @@ const AuthenticatedSettingsAccessLazyRoute = import('./routes/_authenticated/settings/access.lazy').then((d) => d.Route), ) +const AuthenticatedAccountsNewLazyRoute = + AuthenticatedAccountsNewLazyImport.update({ + id: '/accounts/new', + path: '/accounts/new', + getParentRoute: () => AuthenticatedRouteRoute, + } as any).lazy(() => + import('./routes/_authenticated/accounts/new.lazy').then((d) => d.Route), + ) + +const AuthenticatedAccountsIdSettingsLazyRoute = + AuthenticatedAccountsIdSettingsLazyImport.update({ + id: '/accounts/$id/settings', + path: '/accounts/$id/settings', + getParentRoute: () => AuthenticatedRouteRoute, + } as any).lazy(() => + import('./routes/_authenticated/accounts/$id.settings.lazy').then( + (d) => d.Route, + ), + ) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -398,6 +424,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedIndexImport parentRoute: typeof AuthenticatedRouteImport } + '/_authenticated/accounts/new': { + id: '/_authenticated/accounts/new' + path: '/accounts/new' + fullPath: '/accounts/new' + preLoaderRoute: typeof AuthenticatedAccountsNewLazyImport + parentRoute: typeof AuthenticatedRouteImport + } '/_authenticated/settings/access': { id: '/_authenticated/settings/access' path: '/access' @@ -517,6 +550,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedUsersIndexLazyImport parentRoute: typeof AuthenticatedUsersRouteLazyImport } + '/_authenticated/accounts/$id/settings': { + id: '/_authenticated/accounts/$id/settings' + path: '/accounts/$id/settings' + fullPath: '/accounts/$id/settings' + preLoaderRoute: typeof AuthenticatedAccountsIdSettingsLazyImport + parentRoute: typeof AuthenticatedRouteImport + } } } @@ -574,6 +614,7 @@ interface AuthenticatedRouteRouteChildren { AuthenticatedSettingsRouteLazyRoute: typeof AuthenticatedSettingsRouteLazyRouteWithChildren AuthenticatedUsersRouteLazyRoute: typeof AuthenticatedUsersRouteLazyRouteWithChildren AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute + AuthenticatedAccountsNewLazyRoute: typeof AuthenticatedAccountsNewLazyRoute AuthenticatedAttachmentIndexRoute: typeof AuthenticatedAttachmentIndexRoute AuthenticatedImportIndexRoute: typeof AuthenticatedImportIndexRoute AuthenticatedSearchIndexRoute: typeof AuthenticatedSearchIndexRoute @@ -581,6 +622,7 @@ interface AuthenticatedRouteRouteChildren { AuthenticatedApiDocsIndexLazyRoute: typeof AuthenticatedApiDocsIndexLazyRoute AuthenticatedOauth2ResultIndexLazyRoute: typeof AuthenticatedOauth2ResultIndexLazyRoute AuthenticatedOauth2IndexLazyRoute: typeof AuthenticatedOauth2IndexLazyRoute + AuthenticatedAccountsIdSettingsLazyRoute: typeof AuthenticatedAccountsIdSettingsLazyRoute } const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = { @@ -589,6 +631,7 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = { AuthenticatedUsersRouteLazyRoute: AuthenticatedUsersRouteLazyRouteWithChildren, AuthenticatedIndexRoute: AuthenticatedIndexRoute, + AuthenticatedAccountsNewLazyRoute: AuthenticatedAccountsNewLazyRoute, AuthenticatedAttachmentIndexRoute: AuthenticatedAttachmentIndexRoute, AuthenticatedImportIndexRoute: AuthenticatedImportIndexRoute, AuthenticatedSearchIndexRoute: AuthenticatedSearchIndexRoute, @@ -597,6 +640,8 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = { AuthenticatedOauth2ResultIndexLazyRoute: AuthenticatedOauth2ResultIndexLazyRoute, AuthenticatedOauth2IndexLazyRoute: AuthenticatedOauth2IndexLazyRoute, + AuthenticatedAccountsIdSettingsLazyRoute: + AuthenticatedAccountsIdSettingsLazyRoute, } const AuthenticatedRouteRouteWithChildren = @@ -613,6 +658,7 @@ export interface FileRoutesByFullPath { '/404': typeof errors404LazyRoute '/503': typeof errors503LazyRoute '/': typeof AuthenticatedIndexRoute + '/accounts/new': typeof AuthenticatedAccountsNewLazyRoute '/settings/access': typeof AuthenticatedSettingsAccessLazyRoute '/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute '/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute @@ -630,6 +676,7 @@ export interface FileRoutesByFullPath { '/oauth2': typeof AuthenticatedOauth2IndexLazyRoute '/settings/': typeof AuthenticatedSettingsIndexLazyRoute '/users/': typeof AuthenticatedUsersIndexLazyRoute + '/accounts/$id/settings': typeof AuthenticatedAccountsIdSettingsLazyRoute } export interface FileRoutesByTo { @@ -640,6 +687,7 @@ export interface FileRoutesByTo { '/404': typeof errors404LazyRoute '/503': typeof errors503LazyRoute '/': typeof AuthenticatedIndexRoute + '/accounts/new': typeof AuthenticatedAccountsNewLazyRoute '/settings/access': typeof AuthenticatedSettingsAccessLazyRoute '/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute '/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute @@ -657,6 +705,7 @@ export interface FileRoutesByTo { '/oauth2': typeof AuthenticatedOauth2IndexLazyRoute '/settings': typeof AuthenticatedSettingsIndexLazyRoute '/users': typeof AuthenticatedUsersIndexLazyRoute + '/accounts/$id/settings': typeof AuthenticatedAccountsIdSettingsLazyRoute } export interface FileRoutesById { @@ -672,6 +721,7 @@ export interface FileRoutesById { '/(errors)/500': typeof errors500LazyRoute '/(errors)/503': typeof errors503LazyRoute '/_authenticated/': typeof AuthenticatedIndexRoute + '/_authenticated/accounts/new': typeof AuthenticatedAccountsNewLazyRoute '/_authenticated/settings/access': typeof AuthenticatedSettingsAccessLazyRoute '/_authenticated/settings/api-tokens': typeof AuthenticatedSettingsApiTokensLazyRoute '/_authenticated/settings/appearance': typeof AuthenticatedSettingsAppearanceLazyRoute @@ -689,6 +739,7 @@ export interface FileRoutesById { '/_authenticated/oauth2/': typeof AuthenticatedOauth2IndexLazyRoute '/_authenticated/settings/': typeof AuthenticatedSettingsIndexLazyRoute '/_authenticated/users/': typeof AuthenticatedUsersIndexLazyRoute + '/_authenticated/accounts/$id/settings': typeof AuthenticatedAccountsIdSettingsLazyRoute } export interface FileRouteTypes { @@ -704,6 +755,7 @@ export interface FileRouteTypes { | '/404' | '/503' | '/' + | '/accounts/new' | '/settings/access' | '/settings/api-tokens' | '/settings/appearance' @@ -721,6 +773,7 @@ export interface FileRouteTypes { | '/oauth2' | '/settings/' | '/users/' + | '/accounts/$id/settings' fileRoutesByTo: FileRoutesByTo to: | '/500' @@ -730,6 +783,7 @@ export interface FileRouteTypes { | '/404' | '/503' | '/' + | '/accounts/new' | '/settings/access' | '/settings/api-tokens' | '/settings/appearance' @@ -747,6 +801,7 @@ export interface FileRouteTypes { | '/oauth2' | '/settings' | '/users' + | '/accounts/$id/settings' id: | '__root__' | '/_authenticated' @@ -760,6 +815,7 @@ export interface FileRouteTypes { | '/(errors)/500' | '/(errors)/503' | '/_authenticated/' + | '/_authenticated/accounts/new' | '/_authenticated/settings/access' | '/_authenticated/settings/api-tokens' | '/_authenticated/settings/appearance' @@ -777,6 +833,7 @@ export interface FileRouteTypes { | '/_authenticated/oauth2/' | '/_authenticated/settings/' | '/_authenticated/users/' + | '/_authenticated/accounts/$id/settings' fileRoutesById: FileRoutesById } @@ -828,13 +885,15 @@ export const routeTree = rootRoute "/_authenticated/settings", "/_authenticated/users", "/_authenticated/", + "/_authenticated/accounts/new", "/_authenticated/attachment/", "/_authenticated/import/", "/_authenticated/search/", "/_authenticated/accounts/", "/_authenticated/api-docs/", "/_authenticated/oauth2-result/", - "/_authenticated/oauth2/" + "/_authenticated/oauth2/", + "/_authenticated/accounts/$id/settings" ] }, "/(auth)/500": { @@ -884,6 +943,10 @@ export const routeTree = rootRoute "filePath": "_authenticated/index.tsx", "parent": "/_authenticated" }, + "/_authenticated/accounts/new": { + "filePath": "_authenticated/accounts/new.lazy.tsx", + "parent": "/_authenticated" + }, "/_authenticated/settings/access": { "filePath": "_authenticated/settings/access.lazy.tsx", "parent": "/_authenticated/settings" @@ -951,6 +1014,10 @@ export const routeTree = rootRoute "/_authenticated/users/": { "filePath": "_authenticated/users/index.lazy.tsx", "parent": "/_authenticated/users" + }, + "/_authenticated/accounts/$id/settings": { + "filePath": "_authenticated/accounts/$id.settings.lazy.tsx", + "parent": "/_authenticated" } } } diff --git a/web/src/routes/_authenticated/accounts/$id.settings.lazy.tsx b/web/src/routes/_authenticated/accounts/$id.settings.lazy.tsx new file mode 100644 index 0000000..d42de2d --- /dev/null +++ b/web/src/routes/_authenticated/accounts/$id.settings.lazy.tsx @@ -0,0 +1,29 @@ +// +// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { createLazyFileRoute } from '@tanstack/react-router' +import { AccountSettingsPage } from '@/features/accounts/account-settings-page' + +export const Route = createLazyFileRoute('/_authenticated/accounts/$id/settings')({ + component: AccountSettingsWrapper, +}) + +function AccountSettingsWrapper() { + const { id } = Route.useParams() + return +} diff --git a/web/src/routes/_authenticated/accounts/new.lazy.tsx b/web/src/routes/_authenticated/accounts/new.lazy.tsx new file mode 100644 index 0000000..fed8def --- /dev/null +++ b/web/src/routes/_authenticated/accounts/new.lazy.tsx @@ -0,0 +1,24 @@ +// +// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { createLazyFileRoute } from '@tanstack/react-router' +import { AccountNewPage } from '@/features/accounts/account-new' + +export const Route = createLazyFileRoute('/_authenticated/accounts/new')({ + component: AccountNewPage, +})