feat: Add sync_batch_size to allow users to customize the synchronization batch size, and introduce date_before to support semantics such as downloading emails from more than one year ago. #24 #58

This commit is contained in:
rustmailer
2025-12-28 13:01:34 +08:00
parent b35493e4e1
commit 1f57f372d3
46 changed files with 831 additions and 351 deletions
+53 -1
View File
@@ -18,7 +18,6 @@
import axiosInstance from "@/api/axiosInstance";
import { AccountModel } from "@/features/accounts/data/schema";
import { PaginatedResponse } from "..";
export interface MinimalAccount {
@@ -56,6 +55,59 @@ export interface MailboxBatchProgress {
current_batch: number;
}
type Encryption = 'Ssl' | 'StartTls' | 'None';
type AuthType = 'Password' | 'OAuth2';
type Unit = 'Days' | 'Months' | 'Years';
type AccountType = 'IMAP' | 'NoSync';
// Interface definitions
interface AuthConfig {
auth_type: AuthType;
password?: string;
}
export interface ImapConfig {
host: string;
port: number; // integer, 0-65535
encryption: Encryption;
auth: AuthConfig;
use_proxy?: number;
}
interface RelativeDate {
unit: Unit;
value: number; // integer, minimum 1
}
interface DateSelection {
fixed?: string; // format: "YYYY-MM-DD"
relative?: RelativeDate;
}
export interface AccountModel {
id: number;
account_type: AccountType;
imap?: ImapConfig;
enabled: boolean;
name?: string,
email: string;
capabilities?: string[];
date_since?: DateSelection;
date_before?: RelativeDate;
folder_limit?: number,
sync_folders: string[];
sync_interval_min?: number;
sync_batch_size?: number;
created_by: number;
created_user_name: string;
created_user_email: string;
created_at: number;
updated_at: number;
use_proxy?: number
use_dangerous: boolean
}
export const account_state = async (account_id: number) => {
const response = await axiosInstance.get<AccountRunningState>(`/api/v1/account-state/${account_id}`);
return response.data;
@@ -52,11 +52,9 @@ import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Input } from '@/components/ui/input'
import { useToast } from '@/hooks/use-toast'
import { AccountModel } from '../data/schema'
import { useRoles } from '@/hooks/use-roles'
import { useMinimalUsers } from '@/hooks/use-minimal-users'
import { access_assign } from '@/api/account/api'
import { access_assign, AccountModel } from '@/api/account/api'
interface Props {
currentRow: AccountModel
@@ -17,7 +17,6 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { AccountModel } from '../data/schema'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@@ -25,6 +24,7 @@ import { Checkbox } from '@/components/ui/checkbox'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useTranslation } from 'react-i18next'
import { AccountModel } from '@/api/account/api'
interface Props {
open: boolean
@@ -34,6 +34,30 @@ interface Props {
export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
const { t } = useTranslation()
const sinceText = (() => {
if (currentRow.date_since?.fixed) {
return currentRow.date_since.fixed;
}
if (currentRow.date_since?.relative?.value) {
return `${t('accounts.sinceRelativeValue', {
value: currentRow.date_since!.relative!.value,
unit: t(`accounts.${currentRow.date_since!.relative!.unit!.toLowerCase()}`)
})}`;
}
return t('accounts.syncAll');
})();
const hasSince = !!currentRow.date_since;
const hasBefore = !!currentRow.date_before?.value;
return (
<Dialog
open={open}
@@ -77,6 +101,10 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
<span className="text-muted-foreground">{t('accounts.incrementalSyncInterval')}:</span>
<span>{t('accounts.everyMinutes', { minutes: currentRow.sync_interval_min })}</span>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-muted-foreground">{t('accounts.syncBatchSize')}:</span>
<span>{currentRow.sync_batch_size}</span>
</div>
<div className="flex flex-col gap-2">
<span className="text-muted-foreground">{t('accounts.capabilities')}:</span>
<code className="rounded-md bg-muted/50 px-2 py-1 text-sm border overflow-x-auto inline-block">
@@ -84,14 +112,33 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
</code>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-muted-foreground">{t('accounts.dateSelection')}:</span>
<span>
{currentRow.date_since?.fixed
? t('accounts.since') + ' ' + currentRow.date_since.fixed
: currentRow.date_since?.relative
? t('accounts.recent') + ' ' + currentRow.date_since.relative.value + ' ' + currentRow.date_since.relative.unit
: t('accounts.notAvailable')}
</span>
<span className="text-muted-foreground">{t('accounts.syncScope')}:</span>
{hasSince && (
<div className="flex flex-col">
<span className="text-xs text-muted-foreground">
{t('accounts.sinceFixed')}:
</span>
<span className="text-sm">{sinceText}</span>
</div>
)}
{hasBefore && (
<div className="flex flex-col border-t pt-2">
<span className="text-xs text-muted-foreground">
{t('accounts.beforeRelative')}:
</span>
<span className="text-sm">
{t('accounts.beforeRelativeValue', {
value: currentRow.date_before!.value,
unit: t(`accounts.${currentRow.date_before!.unit!.toLowerCase()}`)
})}
</span>
</div>
)}
{!hasSince && !hasBefore && (
<span className="text-sm text-muted-foreground">
{t('accounts.syncAll')}
</span>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-muted-foreground">{t('accounts.folderLimit')}:</span>
@@ -100,8 +147,6 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
</div>
</CardContent>
</Card>
{/* Server Configuration Card */}
<Card>
<CardHeader>
<CardTitle>{t('accounts.serverConfiguration')}</CardTitle>
@@ -143,8 +188,6 @@ export function AccountDetailDrawer({ open, onOpenChange, currentRow }: Props) {
</div>
</CardContent>
</Card>
{/* Sync Folders Card */}
<Card>
<CardHeader>
<CardTitle>{t('accounts.syncFoldersTitle')}</CardTitle>
@@ -16,14 +16,12 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { zodResolver } from '@hookform/resolvers/zod';
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { AccountModel, ImapConfig } from '../data/schema';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useToast } from '@/hooks/use-toast';
@@ -31,11 +29,12 @@ import Step1 from './step1';
import Step2 from './step2';
import Step3 from './step3';
import Step4 from './step4';
import { create_account, autoconfig, update_account } from '@/api/account/api';
import { create_account, autoconfig, update_account, AccountModel, ImapConfig } from '@/api/account/api';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ToastAction } from '@/components/ui/toast';
import { AxiosError } from 'axios';
import { useTranslation } from 'react-i18next';
import { cn } from "@/lib/utils";
const encryptionSchema = z.union([
z.literal('Ssl'),
@@ -80,7 +79,7 @@ const getRelativeDateSchema = (t: (key: string) => string) => z.object({
});
const getDateSelectionSchema = (t: (key: string) => string) => z.union([
z.object({ fixed: z.string({ message: t('accounts.selectDate') }) },),
z.object({ fixed: z.string({ message: t('accounts.selectDate') }) }),
z.object({ relative: getRelativeDateSchema(t) }),
z.undefined(),
]);
@@ -107,8 +106,13 @@ export type Account = {
value?: number;
};
};
date_before?: {
unit?: 'Days' | 'Months' | 'Years';
value?: number;
};
folder_limit?: number;
sync_interval_min: number;
sync_batch_size: number;
};
const getAccountSchema = (isEdit: boolean, t: (key: string) => string) =>
@@ -119,12 +123,18 @@ const getAccountSchema = (isEdit: boolean, t: (key: string) => string) =>
enabled: z.boolean(),
use_dangerous: z.boolean(),
date_since: getDateSelectionSchema(t).optional(),
date_before: getRelativeDateSchema(t).optional(),
folder_limit: z
.number({ invalid_type_error: t('validation.folderLimitMustBeNumber') })
.int()
.min(100, { message: t('validation.folderLimitMustBeAtLeast100') })
.optional(),
sync_interval_min: z.number({ invalid_type_error: t('validation.incrementalSyncMustBeNumber') }).int().min(10, { message: t('validation.incrementalSyncMustBeAtLeast10') }),
sync_batch_size: z
.number({ invalid_type_error: t('validation.incrementalSyncMustBeNumber') })
.int()
.min(30, { message: t('validation.incrementalSyncMustBeAtLeast10') })
.max(200, { message: t('validation.incrementalSyncMustBeAtLeast10') }),
});
type Step = {
@@ -133,14 +143,12 @@ type Step = {
fields: (keyof Account)[];
};
export type Steps = [
...Step[]
];
export type Steps = [...Step[]];
const getSteps = (t: (key: string) => string): Steps => [
{ id: "step-1", name: t('accounts.steps.emailAddress'), fields: ["email"] },
{ id: "step-2", name: t('accounts.steps.imap'), fields: ["imap", "use_dangerous", "name"] },
{ id: "step-3", name: t('accounts.steps.syncPreferences'), fields: ["enabled", "date_since", "folder_limit", "sync_interval_min"] },
{ id: "step-3", name: t('accounts.steps.syncPreferences'), fields: ["enabled", "date_since", "date_before", "folder_limit", "sync_interval_min", "sync_batch_size"] },
{ id: "step-4", name: t('accounts.steps.summary'), fields: [] },
];
@@ -168,8 +176,10 @@ const defaultValues: Account = {
enabled: true,
use_dangerous: false,
date_since: undefined,
date_before: undefined,
folder_limit: undefined,
sync_interval_min: 10,
sync_batch_size: 50,
};
const emptyImap: ImapConfig = {
@@ -194,8 +204,10 @@ const mapCurrentRowToFormValues = (currentRow: AccountModel): Account => {
enabled: currentRow.enabled,
use_dangerous: currentRow.use_dangerous,
date_since: currentRow.date_since ?? undefined,
date_before: currentRow.date_before ?? undefined,
folder_limit: currentRow.folder_limit ?? undefined,
sync_interval_min: currentRow.sync_interval_min ?? 10,
sync_batch_size: currentRow.sync_batch_size ?? 50,
};
};
@@ -272,8 +284,10 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
enabled: data.enabled,
use_dangerous: data.use_dangerous,
date_since: data.date_since,
date_before: data.date_before,
folder_limit: data.folder_limit,
sync_interval_min: data.sync_interval_min,
sync_batch_size: data.sync_batch_size,
};
if (isEdit) {
updateMutation.mutate(commonData);
@@ -330,61 +344,67 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
<Dialog
open={open}
onOpenChange={(state) => {
form.reset();
setCurrentStep(1);
if (!state) {
form.reset();
setCurrentStep(1);
}
onOpenChange(state);
}}
>
<DialogContent className='max-w-5xl'>
<DialogHeader className='text-left mb-4'>
<DialogTitle>{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}</DialogTitle>
<DialogDescription>
{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}
{t('accounts.clickSaveWhenDone')}
</DialogDescription>
</DialogHeader>
<ScrollArea className="h-[38rem] w-full pr-4 -mr-4 py-1">
<>
<div className="flex my-5 space-x-4 md:hidden">
{steps.map((step, index) => (
<DialogContent className="max-w-[95vw] md:max-w-5xl w-full p-0 overflow-hidden flex flex-col h-[90vh]">
<div className="p-6 pb-2 flex-shrink-0">
<DialogHeader className="text-left">
<DialogTitle>{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}</DialogTitle>
<DialogDescription>
{isEdit ? t('accounts.updateAccount') : t('accounts.addAccount')}
{t('accounts.clickSaveWhenDone')}
</DialogDescription>
</DialogHeader>
</div>
<div className="flex flex-col md:flex-row flex-1 min-h-0 overflow-hidden border-y">
<div className="md:hidden flex px-6 py-2 space-x-2 overflow-x-auto border-b flex-shrink-0 bg-background/50">
{steps.map((step, index) => (
<div key={step.id} className="flex flex-col items-center flex-shrink-0 min-w-[70px]">
<Button
key={step.id}
className={`size-9 rounded-full border font-bold ${currentStep === index + 1 ? "bg-primary text-white" : "bg-gray-200 text-black"
}`}
variant={currentStep === index + 1 ? "default" : "secondary"}
className="size-8 rounded-full font-bold p-0"
disabled={currentStep === index + 1}
onClick={() => setCurrentStep(index + 1)}
>
{index + 1}
</Button>
))}
</div>
<div className="w-full max-w-full p-4">
<div className="flex md:h-min rounded-xl md:rounded-2xl p-4">
<div className="hidden md:block w-[260px] flex-shrink-0 rounded-xl p-5 pt-7 fixed">
{steps.map((step, index) => (
<div className="my-3 ml-2 flex items-center" key={step.id}>
<Button
className={`size-8 border rounded-full text-sm font-bold ${currentStep === index + 1 ? "bg-primary text-white" : "bg-gray-200 text-black"
}`}
disabled={currentStep === index + 1}
onClick={() => setCurrentStep(index + 1)}
>
{index + 1}
</Button>
<div className="flex flex-col items-baseline uppercase ml-5">
<span className="text-xs">{t('accounts.step', { index: index + 1 })}</span>
<span className="font-bold text-sm tracking-wider">{step.name}</span>
</div>
</div>
))}
</div>
<span className="text-[10px] mt-1 text-muted-foreground line-clamp-1">{step.name}</span>
</div>
))}
</div>
<div className="hidden md:block w-[240px] flex-shrink-0 px-8 py-4 border-r overflow-y-auto">
{steps.map((step, index) => (
<div className="mb-8 flex items-center" key={step.id}>
<Button
variant={currentStep === index + 1 ? "default" : "secondary"}
className="size-9 rounded-full text-sm font-bold"
disabled={currentStep === index + 1}
onClick={() => setCurrentStep(index + 1)}
>
{index + 1}
</Button>
<div className="flex flex-col items-baseline uppercase ml-4">
<span className="text-[10px] text-muted-foreground">{t('accounts.step', { index: index + 1 })}</span>
<span className={cn("font-bold text-sm tracking-wider", currentStep === index + 1 ? "text-foreground" : "text-muted-foreground")}>
{step.name}
</span>
</div>
</div>
))}
</div>
<div className="flex-1 min-h-0 relative">
<ScrollArea className="h-full w-full">
<div className="p-6 md:p-10 lg:p-14">
<Form {...form}>
<form
id="account-register-form"
className="flex-grow flex flex-col px-4 md:px-8 lg:px-12 ml-[240px]"
onSubmit={form.handleSubmit(onSubmit)}
>
<form id="account-register-form" onSubmit={form.handleSubmit(onSubmit)}>
{currentStep === 1 && <Step1 isEdit={isEdit} />}
{currentStep === 2 && <Step2 isEdit={isEdit} />}
{currentStep === 3 && <Step3 />}
@@ -392,14 +412,16 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
</form>
</Form>
</div>
</div>
</>
</ScrollArea>
<DialogFooter className="flex flex-wrap gap-2">
</ScrollArea>
</div>
</div>
<DialogFooter className="p-4 md:p-6 bg-background flex flex-row sm:justify-end gap-2 flex-shrink-0">
{currentStep > 1 && (
<Button
type="button"
className="flex-grow sm:flex-grow-0 shadow-none text-nowrap text-sm"
variant="outline"
className="flex-1 sm:flex-none"
onClick={() => setCurrentStep(currentStep - 1)}
>
{t('accounts.goBack')}
@@ -408,7 +430,7 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
{currentStep < LAST_STEP && (
<Button
type="button"
className="flex-grow sm:flex-grow-0 rounded-md md:rounded-lg px-6 text-sm"
className="flex-1 sm:flex-none px-8"
onClick={handleContinue}
>
{autoConfigLoading ? t('accounts.autoConfiguring') : t('accounts.continue')}
@@ -418,7 +440,7 @@ export function AccountActionDialog({ currentRow, open, onOpenChange }: Props) {
<Button
type="submit"
form="account-register-form"
className="flex-grow sm:flex-grow-0 rounded-md text-sm px-7 md:rounded-lg"
className="flex-1 sm:flex-none px-10"
>
{isEdit ? t('accounts.saveChanges') : t('accounts.submit')}
</Button>
@@ -19,8 +19,6 @@
import { ColumnDef } from '@tanstack/react-table'
import LongText from '@/components/long-text'
import { AccountModel } from '../data/schema'
import { DataTableColumnHeader } from './data-table-column-header'
import { DataTableRowActions } from './data-table-row-actions'
import { format } from 'date-fns'
@@ -28,6 +26,7 @@ import { OAuth2Action } from './oauth2-action'
import { RunningStateCellAction } from './running-state-action'
import { EnableAction } from './enable-action'
import { useTranslation } from 'react-i18next'
import { AccountModel } from '@/api/account/api'
export function useColumns(): ColumnDef<AccountModel>[] {
const { t } = useTranslation()
@@ -112,7 +111,7 @@ export function useColumns(): ColumnDef<AccountModel>[] {
{
accessorKey: 'created_by',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Owner" className="justify-center" />
<DataTableColumnHeader column={column} title={t('accounts.owner')} className="justify-center" />
),
cell: ({ row }) => {
const { created_user_name, created_user_email } = row.original;
@@ -30,10 +30,10 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useAccountContext } from '../context'
import { AccountModel } from '../data/schema'
import { Mailbox, MessageSquareMore } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useCurrentUser } from '@/hooks/use-current-user'
import { AccountModel } from '@/api/account/api'
interface DataTableRowActionsProps {
row: Row<AccountModel>
@@ -113,7 +113,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
setOpen('access-assign')
}}
>
<span>Access Control</span>
<span>{t('accounts.accessControl')}</span>
<DropdownMenuShortcut>
<IconShieldLock size={16} />
</DropdownMenuShortcut>
@@ -24,11 +24,10 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { AccountModel } from '../data/schema'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { ToastAction } from '@/components/ui/toast'
import { AxiosError } from 'axios'
import { remove_account } from '@/api/account/api'
import { AccountModel, remove_account } from '@/api/account/api'
import { useTranslation } from 'react-i18next'
interface Props {
@@ -18,13 +18,12 @@
import { Row } from '@tanstack/react-table'
import { AccountModel } from '../data/schema'
import { Switch } from '@/components/ui/switch'
import { useState } from 'react'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { ToastAction } from '@/components/ui/toast'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { update_account } from '@/api/account/api'
import { AccountModel, update_account } from '@/api/account/api'
import { toast } from '@/hooks/use-toast'
import { AxiosError } from 'axios'
import { useTranslation } from 'react-i18next'
@@ -28,12 +28,11 @@ import { AxiosError } from 'axios';
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { create_account, update_account } from '@/api/account/api';
import { AccountModel, create_account, update_account } from '@/api/account/api';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Checkbox } from '@/components/ui/checkbox';
import { Loader2 } from 'lucide-react';
import { AccountModel } from '../data/schema';
import { useTranslation } from 'react-i18next';
@@ -20,11 +20,11 @@
import { Row } from '@tanstack/react-table'
import { Button } from '@/components/ui/button'
import { useAccountContext } from '../context'
import { AccountModel } from '../data/schema'
import { useTranslation } from 'react-i18next'
import { useCurrentUser } from '@/hooks/use-current-user'
import { toast } from '@/hooks/use-toast'
import { ToastAction } from '@/components/ui/toast'
import { AccountModel } from '@/api/account/api'
interface DataTableRowActionsProps {
row: Row<AccountModel>
@@ -26,7 +26,6 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { AccountModel } from '../data/schema'
import { Button } from '@/components/ui/button'
import { get_oauth2_tokens } from '@/api/oauth2/api'
import { useQuery } from '@tanstack/react-query'
@@ -44,6 +43,7 @@ import { ToastAction } from '@/components/ui/toast'
import { useNavigate } from '@tanstack/react-router'
import { dateFnsLocaleMap } from '@/lib/utils'
import { enUS } from 'date-fns/locale'
import { AccountModel } from '@/api/account/api'
interface Props {
currentRow: AccountModel
@@ -19,12 +19,12 @@
import { Row } from '@tanstack/react-table'
import { Button } from '@/components/ui/button'
import { AccountModel } from '../data/schema';
import { useAccountContext } from '../context';
import { useTranslation } from 'react-i18next';
import { useCurrentUser } from '@/hooks/use-current-user';
import { toast } from '@/hooks/use-toast';
import { ToastAction } from '@/components/ui/toast';
import { AccountModel } from '@/api/account/api';
interface Props {
row: Row<AccountModel>
@@ -25,9 +25,8 @@ import {
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { AccountModel } from '../data/schema'
import { useQuery } from '@tanstack/react-query'
import { account_state } from '@/api/account/api'
import { account_state, AccountModel } from '@/api/account/api'
import { formatDistanceToNow, formatDuration, intervalToDuration } from 'date-fns'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton'
+167 -150
View File
@@ -39,189 +39,206 @@ import { Button } from "@/components/ui/button";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import { Calendar } from "@/components/ui/calendar";
import { cn } from "@/lib/utils";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { cn, dateFnsLocaleMap } from "@/lib/utils";
import { useState } from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { useTranslation } from "react-i18next";
import { enUS } from "date-fns/locale";
import i18n from "@/i18n";
type SyncMode = 'all' | 'since_fixed' | 'since_relative' | 'before_relative';
export default function Step3() {
const { t } = useTranslation();
const { control, getValues, setValue } = useFormContext<Account>();
const current = getValues();
const [rangeType, setRangeType] = useState<'none' | 'fixed' | 'relative'>(
current.date_since ? (current.date_since.fixed ? 'fixed' : 'relative') : 'none'
);
const [syncMode, setSyncMode] = useState<SyncMode>(() => {
if (current.date_before) return 'before_relative';
if (current.date_since?.fixed) return 'since_fixed';
if (current.date_since?.relative) return 'since_relative';
return 'all';
});
const handleModeChange = (mode: SyncMode) => {
setSyncMode(mode);
setValue("date_since", undefined);
setValue("date_before", undefined);
if (mode === 'since_fixed') {
setValue("date_since.fixed", undefined);
} else if (mode === 'since_relative') {
setValue("date_since.relative", { value: 1, unit: 'Months' });
} else if (mode === 'before_relative') {
setValue("date_before", { value: 1, unit: 'Years' });
}
};
return (
<div className="space-y-8">
<FormField
control={control}
name="sync_interval_min"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center justify-between">
{t('accounts.incrementalSync')}:
</FormLabel>
<FormControl>
<Input
type="number"
placeholder={t('accounts.incrementalSyncPlaceholder')}
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value, 10))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField
control={control}
name="sync_interval_min"
render={({ field }) => (
<FormItem>
<FormLabel>{t('accounts.incrementalSync')}</FormLabel>
<FormControl>
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
</FormControl>
<FormMessage />
<FormDescription>
{t('accounts.incrementalSyncDescription')}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={control}
name="sync_batch_size"
render={({ field }) => (
<FormItem>
<FormLabel>{t('accounts.syncBatchSize')}</FormLabel>
<FormControl>
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
</FormControl>
<FormMessage />
<FormDescription>
{t('accounts.syncBatchSizeDescription')}
</FormDescription>
</FormItem>
)}
/>
</div>
<FormField
control={control}
name="enabled"
render={({ field }) => (
<FormItem className="flex flex-col items-start gap-y-1">
<FormLabel>{t('accounts.enabled')}:</FormLabel>
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4 shadow-sm">
<FormControl>
<Checkbox
className="mt-2"
checked={field.value}
onCheckedChange={field.onChange}
/>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<FormDescription>{t('accounts.enabledDescription')}</FormDescription>
<div className="space-y-1 leading-none">
<FormLabel>{t('accounts.enabled')}</FormLabel>
<FormDescription>{t('accounts.enabledDescription')}</FormDescription>
</div>
</FormItem>
)}
/>
<FormLabel className="flex items-center justify-between">{t('accounts.dateSince')}:</FormLabel>
<RadioGroup
defaultValue={rangeType}
onValueChange={(value: 'fixed' | 'relative' | 'none') => {
setRangeType(value);
if (value === 'none') {
setValue("date_since", undefined, { shouldValidate: true });
}
if (value === 'fixed') {
setValue("date_since", { fixed: undefined }, { shouldValidate: true });
}
if (value === 'relative') {
setValue("date_since", { relative: { value: undefined, unit: undefined } }, { shouldValidate: true });
}
}}
className="flex flex-row space-x-4"
>
<FormItem className="flex items-center space-x-3">
<RadioGroupItem value="none" />
<FormLabel className="font-normal">{t('accounts.none')}</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3">
<RadioGroupItem value="fixed" />
<FormLabel className="font-normal">{t('accounts.fixed')}</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3">
<RadioGroupItem value="relative" />
<FormLabel className="font-normal">{t('accounts.relative')}</FormLabel>
</FormItem>
</RadioGroup>
<FormDescription>
{t('accounts.syncStartDateDescription', {
fixedPart: rangeType === 'fixed' ? t('accounts.syncAfterDate') : t('accounts.syncRecentData'),
})}
</FormDescription>
<hr className="my-4" />
<div className="space-y-4">
<FormItem>
<FormLabel className="text-base font-semibold">{t('accounts.syncScope', 'Sync Strategy')}</FormLabel>
<FormDescription>
{t('accounts.syncScopeDescription', 'Choose which emails should be indexed and archived.')}
</FormDescription>
<Select value={syncMode} onValueChange={(v) => handleModeChange(v as SyncMode)}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t('accounts.selectMode')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t('accounts.syncAll', 'Sync All Emails')}</SelectItem>
<SelectItem value="since_fixed">{t('accounts.sinceFixed', 'Since Specific Date')}</SelectItem>
<SelectItem value="since_relative">{t('accounts.sinceRelative', 'Keep Recent Emails')}</SelectItem>
<SelectItem value="before_relative">{t('accounts.beforeRelative', 'Archive Old Emails Only')}</SelectItem>
</SelectContent>
</Select>
</FormItem>
<div className="pl-2 border-l-2 border-primary/20 space-y-4 pt-2">
{syncMode === 'since_fixed' && (
<FormField
control={control}
name="date_since.fixed"
render={({ field }) => {
const currentLang = i18n.language.toLowerCase().replace('_', '-');
const dateLocale = dateFnsLocaleMap[currentLang] || enUS;
return <FormItem className="flex flex-col">
<FormLabel>{t('accounts.selectDate')}</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn("w-[440px] pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
>
{field.value ? format(new Date(field.value), "PPP", { locale: dateLocale }) : <span>{t('accounts.selectDate')}</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value ? new Date(field.value) : undefined}
onSelect={(date) => field.onChange(date?.toLocaleDateString('en-CA'))}
disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
locale={dateLocale}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>;
{rangeType === 'fixed' && (
<FormField
control={control}
name="date_since.fixed"
render={({ field }) => (
<FormItem className="flex flex-col">
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-[240px] pl-3 text-left font-normal text-brand-marine-blue",
!field.value && "text-muted-foreground"
)}
>
{field.value ? format(field.value, "PPP") : <span>{t('accounts.selectDate')}</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value ? new Date(new Date(field.value).setHours(0, 0, 0, 0)) : undefined}
onSelect={(value) => {
if (value) {
const formattedDate = value.toLocaleDateString('en-CA');
field.onChange(formattedDate);
} else {
field.onChange(null);
}
}}
disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
}}
/>
)}
/>
)}
{rangeType === 'relative' && (
<div className="flex flex-row gap-4">
<div className="flex-1">
<FormField
control={control}
name="date_since.relative.value"
render={({ field }) => (
<FormItem>
<FormControl>
<Input type="number" placeholder="e.g. 1" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="w-1/2">
<FormField
control={control}
name="date_since.relative.unit"
render={({ field }) => (
<FormItem>
<Select onValueChange={field.onChange} defaultValue={field.value}>
{(syncMode === 'since_relative' || syncMode === 'before_relative') && (
<div className="flex flex-row items-end gap-4 animate-in fade-in slide-in-from-left-2">
<FormField
control={control}
name={syncMode === 'since_relative' ? "date_since.relative.value" : "date_before.value"}
render={({ field }) => (
<FormItem className="flex-1 max-w-[150px]">
<FormLabel>{t('accounts.duration', 'Duration')}</FormLabel>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('accounts.selectUnit')} />
</SelectTrigger>
<Input type="number" {...field} onChange={(e) => field.onChange(parseInt(e.target.value, 10))} />
</FormControl>
<SelectContent>
<SelectItem value="Days">{t('accounts.days')}</SelectItem>
<SelectItem value="Months">{t('accounts.months')}</SelectItem>
<SelectItem value="Years">{t('accounts.years')}</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={syncMode === 'since_relative' ? "date_since.relative.unit" : "date_before.unit"}
render={({ field }) => (
<FormItem className="w-[180px]">
<FormLabel>{t('accounts.unit', 'Unit')}</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('accounts.selectUnit')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="Days">{t('accounts.days')}</SelectItem>
<SelectItem value="Months">{t('accounts.months')}</SelectItem>
<SelectItem value="Years">{t('accounts.years')}</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</div>
)}
</div>
<hr className="my-4" />
<FormField
control={control}
name="folder_limit"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center justify-between">{t('accounts.folderLimit')}:</FormLabel>
<FormLabel>{t('accounts.folderLimit')}</FormLabel>
<FormDescription>{t('accounts.folderLimitDescription')}</FormDescription>
<FormControl>
<Input
@@ -237,4 +254,4 @@ export default function Step3() {
/>
</div>
);
}
}
+60 -9
View File
@@ -27,9 +27,28 @@ export default function Step4() {
const { getValues } = useFormContext<Account>();
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 (
<div className="p-5 rounded-xl">
<Accordion type="multiple" defaultValue={['email', 'name', 'imap', 'date_since', 'folder_limit', 'sync_interval']}>
<Accordion type="multiple" defaultValue={['email', 'name', 'imap', 'date_since', 'folder_limit', 'sync_interval', 'sync_scope', 'sync_batch_size']}>
<AccordionItem key="email" value="email">
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.email')}:</AccordionTrigger>
<AccordionContent>{summaryData.email}</AccordionContent>
@@ -82,17 +101,44 @@ export default function Step4() {
</AccordionContent>
</AccordionItem>
<AccordionItem key="date_since" value="date_since">
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.dateSelection')}:</AccordionTrigger>
<AccordionContent>
{summaryData.date_since?.fixed
? t('accounts.since') + ' ' + summaryData.date_since.fixed
: summaryData.date_since?.relative && summaryData.date_since.relative.value && summaryData.date_since.relative.unit
? t('accounts.recent') + ' ' + summaryData.date_since.relative.value + ' ' + summaryData.date_since.relative.unit
: t('accounts.notAvailable')}
<AccordionItem key="sync_scope" value="sync_scope">
<AccordionTrigger className="font-medium capitalize text-gray-600">
{t('accounts.syncScope')}:
</AccordionTrigger>
<AccordionContent className="space-y-3">
{hasSince && (
<div className="flex flex-col">
<span className="text-xs text-muted-foreground">
{t('accounts.sinceFixed')}:
</span>
<span className="text-sm">{sinceText}</span>
</div>
)}
{hasBefore && (
<div className="flex flex-col border-t pt-2">
<span className="text-xs text-muted-foreground">
{t('accounts.beforeRelative')}:
</span>
<span className="text-sm">
{t('accounts.beforeRelativeValue', {
value: summaryData.date_before!.value,
unit: t(`accounts.${summaryData.date_before!.unit!.toLowerCase()}`)
})}
</span>
</div>
)}
{!hasSince && !hasBefore && (
<span className="text-sm text-muted-foreground">
{t('accounts.syncAll')}
</span>
)}
</AccordionContent>
</AccordionItem>
<AccordionItem key="folder_limit" value="folder_limit">
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.folderLimit')}:</AccordionTrigger>
<AccordionContent>{summaryData.folder_limit ?? t('accounts.notAvailable')}</AccordionContent>
@@ -102,6 +148,11 @@ export default function Step4() {
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.incrementalSync')}:</AccordionTrigger>
<AccordionContent>{summaryData.sync_interval_min} {t('accounts.minutes')}</AccordionContent>
</AccordionItem>
<AccordionItem key="sync_batch_size" value="sync_batch_size">
<AccordionTrigger className="font-medium capitalize text-gray-600">{t('accounts.syncBatchSize')}:</AccordionTrigger>
<AccordionContent>{summaryData.sync_batch_size}</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
@@ -29,12 +29,11 @@ import { Button } from '@/components/ui/button'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Loader2, CheckSquare, Square } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { AccountModel } from '../data/schema'
import { toast } from '@/hooks/use-toast'
import { list_mailboxes, MailboxData } from '@/api/mailbox/api'
import { buildTree, ExtendedTreeItemProps } from '@/lib/build-tree'
import { Skeleton } from '@/components/ui/skeleton'
import { update_account } from '@/api/account/api'
import { AccountModel, update_account } from '@/api/account/api'
import { ToastAction } from '@/components/ui/toast'
import axios, { AxiosError } from 'axios'
import { ScrollArea } from '@/components/ui/scroll-area'
@@ -41,10 +41,10 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table'
import { AccountModel } from '../data/schema'
import { DataTablePagination } from './data-table-pagination'
import { DataTableToolbar } from './data-table-toolbar'
import { useTranslation } from 'react-i18next'
import { AccountModel } from '@/api/account/api'
declare module '@tanstack/react-table' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
+1 -1
View File
@@ -17,8 +17,8 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { AccountModel } from '@/api/account/api';
import React from 'react'
import { AccountModel } from '../data/schema'
export type AccountDialogType =
| 'add-imap'
-67
View File
@@ -1,67 +0,0 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
type Encryption = 'Ssl' | 'StartTls' | 'None';
type AuthType = 'Password' | 'OAuth2';
type Unit = 'Days' | 'Months' | 'Years';
type AccountType = 'IMAP' | 'NoSync';
// Interface definitions
interface AuthConfig {
auth_type: AuthType;
password?: string;
}
export interface ImapConfig {
host: string;
port: number; // integer, 0-65535
encryption: Encryption;
auth: AuthConfig;
use_proxy?: number;
}
interface RelativeDate {
unit: Unit;
value: number; // integer, minimum 1
}
interface DateSelection {
fixed?: string; // format: "YYYY-MM-DD"
relative?: RelativeDate;
}
export interface AccountModel {
id: number;
account_type: AccountType;
imap?: ImapConfig;
enabled: boolean;
name?: string,
email: string;
capabilities?: string[];
date_since?: DateSelection;
folder_limit?: number,
sync_folders: string[];
sync_interval_min?: number;
created_by: number;
created_user_name: string;
created_user_email: string;
created_at: number;
updated_at: number;
use_proxy?: number
use_dangerous: boolean
}
+1 -2
View File
@@ -30,9 +30,8 @@ import AccountProvider, {
} from './context'
import { MoreVertical, Plus } from 'lucide-react'
import Logo from '@/assets/logo.svg'
import { AccountModel } from './data/schema'
import { AccountDetailDrawer } from './components/account-detail'
import { list_accounts } from '@/api/account/api'
import { AccountModel, list_accounts } from '@/api/account/api'
import { TableSkeleton } from '@/components/table-skeleton'
import { useQuery } from '@tanstack/react-query'
import { OAuth2TokensDialog } from './components/oauth2-tokens'
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "إصدار النظام"
},
"accounts": {
"beforeRelativeValue": "مزامنة رسائل البريد الإلكتروني قبل {{value}} {{unit}} مضت",
"sinceRelativeValue": "مزامنة رسائل البريد الإلكتروني لآخر {{value}} {{unit}}",
"syncBatchSize": "حجم دفعة المزامنة",
"syncBatchSizeDescription": "عدد الرسائل التي يتم جلبها لكل طلب IMAP",
"incrementalSyncDescription": "عدد مرات إجراء مزامنة البريد الإلكتروني المتزايدة (بالدقائق)",
"syncScope": "استراتيجية المزامنة",
"syncScopeDescription": "اختر رسائل البريد الإلكتروني التي يجب فهرستها وأرشفتها.",
"selectMode": "حدد وضع التصفية",
"syncAll": "مزامنة جميع رسائل البريد الإلكتروني",
"sinceFixed": "منذ تاريخ محدد",
"sinceRelative": "مزامنة رسائل البريد الإلكتروني الحديثة فقط",
"beforeRelative": "أرشفة رسائل البريد الإلكتروني القديمة فقط",
"duration": "المدة",
"unit": "الوحدة",
"accessControl": "التحكم في الوصول",
"owner": "المنشئ",
"access_control": {
"title": "تخصيص الوصول للحساب",
"description": "تعيين الأدوار والمستخدمين المفوضين لـ {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Systemversion"
},
"accounts": {
"beforeRelativeValue": "Synkroniser e-mails fra før {{value}} {{unit}} siden",
"sinceRelativeValue": "Synkroniser e-mails fra de seneste {{value}} {{unit}}",
"syncBatchSize": "Synkroniseringsbatchstørrelse",
"syncBatchSizeDescription": "Antal beskeder hentet per IMAP-forespørgsel",
"incrementalSyncDescription": "Hvor ofte inkrementel e-mail-synkronisering udføres (i minutter)",
"syncScope": "Synkroniseringsstrategi",
"syncScopeDescription": "Vælg hvilke e-mails der skal indekseres og arkiveres.",
"selectMode": "Vælg filtertilstand",
"syncAll": "Synkroniser alle e-mails",
"sinceFixed": "Siden en bestemt dato",
"sinceRelative": "Synkroniser kun nylige e-mails",
"beforeRelative": "Arkiver kun gamle e-mails",
"duration": "Varighed",
"unit": "Enhed",
"accessControl": "Adgangskontrol",
"owner": "Oprettet af",
"access_control": {
"title": "Tildeling af kontoadgang",
"description": "Tildel roller og autoriserede brugere til {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Systemversion"
},
"accounts": {
"beforeRelativeValue": "E-Mails synchronisieren, die älter als {{value}} {{unit}} sind",
"sinceRelativeValue": "E-Mails der letzten {{value}} {{unit}} synchronisieren",
"syncBatchSize": "Synchronisations-Batch-Größe",
"syncBatchSizeDescription": "Anzahl der pro IMAP-Anfrage abgerufenen Nachrichten",
"incrementalSyncDescription": "Häufigkeit der inkrementellen E-Mail-Synchronisierung (in Minuten)",
"syncScope": "Synchronisationsstrategie",
"syncScopeDescription": "Wählen Sie aus, welche E-Mails indiziert und archiviert werden sollen.",
"selectMode": "Filtermodus auswählen",
"syncAll": "Alle E-Mails synchronisieren",
"sinceFixed": "Seit einem bestimmten Datum",
"sinceRelative": "Nur aktuelle E-Mails synchronisieren",
"beforeRelative": "Nur alte E-Mails archivieren",
"duration": "Dauer",
"unit": "Einheit",
"accessControl": "Zugriffskontrolle",
"owner": "Ersteller",
"access_control": {
"title": "Kontozugriffszuweisung",
"description": "Rollen und autorisierte Benutzer für {{email}} zuweisen",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "System Version"
},
"accounts": {
"beforeRelativeValue": "Sync emails before {{value}} {{unit}} ago",
"sinceRelativeValue": "Sync emails from the last",
"syncBatchSize": "Sync batch size",
"syncBatchSizeDescription": "Number of messages fetched per IMAP request",
"incrementalSyncDescription": "How often incremental email synchronization is performed (in minutes)",
"syncScopeDescription": "Choose which emails should be indexed and archived.",
"syncScope": "Sync Strategy",
"selectMode": "Select filter mode",
"syncAll": "Sync All Emails",
"sinceFixed": "Since Specific Date",
"sinceRelative": "Sync Recent Emails Only",
"beforeRelative": "Archive Old Emails Only",
"duration": "Duration",
"unit": "Unit",
"accessControl": "Access Control",
"owner": "Creator",
"access_control": {
"title": "Account Access Assignment",
"description": "Assign roles and authorized users to {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Versión del sistema"
},
"accounts": {
"beforeRelativeValue": "Sincronizar correos de hace más de {{value}} {{unit}}",
"sinceRelativeValue": "Sincronizar correos de los últimos {{value}} {{unit}}",
"syncBatchSize": "Tamaño del lote de sincronización",
"syncBatchSizeDescription": "Número de mensajes obtenidos por solicitud IMAP",
"incrementalSyncDescription": "Frecuencia de sincronización incremental (en minutos)",
"syncScope": "Estrategia de sincronización",
"syncScopeDescription": "Elija qué correos deben indexarse y archivarse.",
"selectMode": "Seleccionar modo de filtro",
"syncAll": "Sincronizar todos los correos",
"sinceFixed": "Desde una fecha específica",
"sinceRelative": "Sincronizar solo correos recientes",
"beforeRelative": "Archivar solo correos antiguos",
"duration": "Duración",
"unit": "Unidad",
"accessControl": "Control de acceso",
"owner": "Creador",
"access_control": {
"title": "Asignación de Acceso a la Cuenta",
"description": "Asignar roles y usuarios autorizados a {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Järjestelmäversio"
},
"accounts": {
"beforeRelativeValue": "Synkronoi sähköpostit, jotka ovat vanhempia kuin {{value}} {{unit}}",
"sinceRelativeValue": "Synkronoi viimeisimmän {{value}} {{unit}} sähköpostit",
"syncBatchSize": "Synkronoinnin eräkoko",
"syncBatchSizeDescription": "Per IMAP-pyyntö noudettujen viestien määrä",
"incrementalSyncDescription": "Kuinka usein inkrementaalinen sähköpostin synkronointi suoritetaan (minuutteina)",
"syncScope": "Synkronointistrategia",
"syncScopeDescription": "Valitse mitkä sähköpostit indeksoidaan ja arkistoidaan.",
"selectMode": "Valitse suodatustila",
"syncAll": "Synkronoi kaikki sähköpostit",
"sinceFixed": "Tietystä päivämäärästä lähtien",
"sinceRelative": "Synkronoi vain viimeisimmät sähköpostit",
"beforeRelative": "Arkistoi vain vanhat sähköpostit",
"duration": "Kesto",
"unit": "Yksikkö",
"accessControl": "Pääsynhallinta",
"owner": "Luoja",
"access_control": {
"title": "Tilin pääsynhallinta",
"description": "Määritä roolit ja valtuutetut käyttäjät kohteelle {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Version du système"
},
"accounts": {
"beforeRelativeValue": "Synchroniser les e-mails datant de plus de {{value}} {{unit}}",
"sinceRelativeValue": "Synchroniser les e-mails des derniers {{value}} {{unit}}",
"syncBatchSize": "Taille du lot de synchronisation",
"syncBatchSizeDescription": "Nombre de messages récupérés par requête IMAP",
"incrementalSyncDescription": "Fréquence de synchronisation incrémentielle (en minutes)",
"syncScope": "Stratégie de synchronisation",
"syncScopeDescription": "Choisissez les e-mails à indexer et à archiver.",
"selectMode": "Sélectionner le mode de filtrage",
"syncAll": "Synchroniser tous les e-mails",
"sinceFixed": "Depuis une date spécifique",
"sinceRelative": "Synchroniser uniquement les e-mails récents",
"beforeRelative": "Archiver uniquement les anciens e-mails",
"duration": "Durée",
"unit": "Unité",
"accessControl": "Contrôle d'accès",
"owner": "Créateur",
"access_control": {
"title": "Attribution d'accès au compte",
"description": "Attribuer des rôles et des utilisateurs autorisés à {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Versione del sistema"
},
"accounts": {
"beforeRelativeValue": "Sincronizza le email antecedenti a {{value}} {{unit}} fa",
"sinceRelativeValue": "Sincronizza le email degli ultimi {{value}} {{unit}}",
"syncBatchSize": "Dimensione batch di sincronizzazione",
"syncBatchSizeDescription": "Numero di messaggi recuperati per richiesta IMAP",
"incrementalSyncDescription": "Frequenza della sincronizzazione incrementale (in minuti)",
"syncScope": "Strategia di sincronizzazione",
"syncScopeDescription": "Scegli quali email indicizzare e archiviare.",
"selectMode": "Seleziona modalità filtro",
"syncAll": "Sincronizza tutte le email",
"sinceFixed": "Da una data specifica",
"sinceRelative": "Sincronizza solo email recenti",
"beforeRelative": "Archivia solo email vecchie",
"duration": "Durata",
"unit": "Unità",
"accessControl": "Controllo accessi",
"owner": "Creatore",
"access_control": {
"title": "Assegnazione Accesso Account",
"description": "Assegna ruoli e utenti autorizzati a {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "システムバージョン"
},
"accounts": {
"beforeRelativeValue": "{{value}} {{unit}} 前より前のメールを同期",
"sinceRelativeValue": "過去 {{value}} {{unit}} 分のメールを同期",
"syncBatchSize": "同期バッチサイズ",
"syncBatchSizeDescription": "1回のIMAPリクエストで取得するメッセージ数",
"incrementalSyncDescription": "増分メール同期の実行頻度(分単位)",
"syncScope": "同期戦略",
"syncScopeDescription": "インデックスを作成し、アーカイブするメールを選択します。",
"selectMode": "フィルタモードを選択",
"syncAll": "すべてのメールを同期",
"sinceFixed": "指定した日付以降",
"sinceRelative": "最近のメールのみ同期",
"beforeRelative": "古いメールのみアーカイブ",
"duration": "期間",
"unit": "単位",
"accessControl": "アクセス制御",
"owner": "作成者",
"access_control": {
"title": "アカウントアクセス割り当て",
"description": "{{email}} にロールと権限ユーザーを割り当てます",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "시스템 버전"
},
"accounts": {
"beforeRelativeValue": "{{value}} {{unit}} 전 이전 이메일 동기화",
"sinceRelativeValue": "지난 {{value}} {{unit}} 동안의 이메일 동기화",
"syncBatchSize": "동기화 배치 크기",
"syncBatchSizeDescription": "IMAP 요청당 가져올 메시지 수",
"incrementalSyncDescription": "증분 이메일 동기화 수행 빈도 (분 단위)",
"syncScope": "동기화 전략",
"syncScopeDescription": "인덱싱 및 아카이빙할 이메일을 선택하십시오.",
"selectMode": "필터 모드 선택",
"syncAll": "모든 이메일 동기화",
"sinceFixed": "특정 날짜 이후",
"sinceRelative": "최신 이메일만 동기화",
"beforeRelative": "오래된 이메일만 아카이브",
"duration": "기간",
"unit": "단위",
"accessControl": "액세스 제어",
"owner": "생성자",
"access_control": {
"title": "계정 액세스 할당",
"description": "{{email}}에 역할 및 권한 사용자를 할당합니다",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Systeemversie"
},
"accounts": {
"beforeRelativeValue": "Synchroniseer e-mails van vóór {{value}} {{unit}} geleden",
"sinceRelativeValue": "Synchroniseer e-mails van de afgelopen {{value}} {{unit}}",
"syncBatchSize": "Batchgrootte synchronisatie",
"syncBatchSizeDescription": "Aantal berichten opgehaald per IMAP-verzoek",
"incrementalSyncDescription": "Frequentie van incrementele synchronisatie (in minuten)",
"syncScope": "Synchronisatiestrategie",
"syncScopeDescription": "Kies welke e-mails geïndexeerd en gearchiveerd moeten worden.",
"selectMode": "Filtermodus selecteren",
"syncAll": "Alle e-mails synchroniseren",
"sinceFixed": "Sinds een specifieke datum",
"sinceRelative": "Alleen recente e-mails synchroniseren",
"beforeRelative": "Alleen oude e-mails archiveren",
"duration": "Duur",
"unit": "Eenheid",
"accessControl": "Toegangsbeheer",
"owner": "Maker",
"access_control": {
"title": "Toewijzing accounttoegang",
"description": "Rollen en geautoriseerde gebruikers toewijzen aan {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Systemversjon"
},
"accounts": {
"beforeRelativeValue": "Synkroniser e-poster fra før {{value}} {{unit}} siden",
"sinceRelativeValue": "Synkroniser e-poster fra de siste {{value}} {{unit}}",
"syncBatchSize": "Synkroniserings-batchstørrelse",
"syncBatchSizeDescription": "Antall meldinger hentet per IMAP-forespørsel",
"incrementalSyncDescription": "Hvor ofte inkrementell e-post-synkronisering utføres (i minutter)",
"syncScope": "Synkroniseringsstrategi",
"syncScopeDescription": "Velg hvilke e-poster som skal indekseres og arkiveres.",
"selectMode": "Velg filtermodus",
"syncAll": "Synkroniser alle e-poster",
"sinceFixed": "Siden spesifikk dato",
"sinceRelative": "Synkroniser kun nylige e-poster",
"beforeRelative": "Arkiver kun gamle e-poster",
"duration": "Varighet",
"unit": "Enhet",
"accessControl": "Tilgangskontroll",
"owner": "Opprettet av",
"access_control": {
"title": "Tildeling av kontotilgang",
"description": "Tildel roller og autoriserte brukere til {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Wersja systemu"
},
"accounts": {
"beforeRelativeValue": "Synchronizuj wiadomości sprzed {{value}} {{unit}}",
"sinceRelativeValue": "Synchronizuj wiadomości z ostatnich {{value}} {{unit}}",
"syncBatchSize": "Rozmiar partii synchronizacji",
"syncBatchSizeDescription": "Liczba wiadomości pobieranych w jednym żądaniu IMAP",
"incrementalSyncDescription": "Częstotliwość wykonywania przyrostowej synchronizacji e-mail (w minutach)",
"syncScope": "Strategia synchronizacji",
"syncScopeDescription": "Wybierz wiadomości e-mail, które mają być indeksowane i archiwizowane.",
"selectMode": "Wybierz tryb filtrowania",
"syncAll": "Synchronizuj wszystkie wiadomości",
"sinceFixed": "Od określonej daty",
"sinceRelative": "Synchronizuj tylko ostatnie wiadomości",
"beforeRelative": "Archiwizuj tylko stare wiadomości",
"duration": "Czas trwania",
"unit": "Jednostka",
"accessControl": "Kontrola dostępu",
"owner": "Twórca",
"access_control": {
"title": "Przypisywanie dostępu do konta",
"description": "Przypisz role i uprawnionych użytkowników dla {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Versão do sistema"
},
"accounts": {
"beforeRelativeValue": "Sincronizar e-mails de antes de {{value}} {{unit}} atrás",
"sinceRelativeValue": "Sincronizar e-mails dos últimos {{value}} {{unit}}",
"syncBatchSize": "Tamanho do lote de sincronização",
"syncBatchSizeDescription": "Número de mensagens obtidas por solicitação IMAP",
"incrementalSyncDescription": "Frequência da sincronização incremental (em minutos)",
"syncScope": "Estratégia de sincronização",
"syncScopeDescription": "Escolha quais e-mails devem ser indexados e arquivados.",
"selectMode": "Selecionar modo de filtro",
"syncAll": "Sincronizar todos os e-mails",
"sinceFixed": "Desde uma data específica",
"sinceRelative": "Sincronizar apenas e-mails recentes",
"beforeRelative": "Arquivar apenas e-mails antigos",
"duration": "Duração",
"unit": "Unidade",
"accessControl": "Controle de acesso",
"owner": "Criador",
"access_control": {
"title": "Atribuição de Acesso à Conta",
"description": "Atribuir funções e usuários autorizados a {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Версия системы"
},
"accounts": {
"beforeRelativeValue": "Синхронизировать письма старее, чем {{value}} {{unit}} назад",
"sinceRelativeValue": "Синхронизировать письма за последние {{value}} {{unit}}",
"syncBatchSize": "Размер пакета синхронизации",
"syncBatchSizeDescription": "Количество сообщений, получаемых за один запрос IMAP",
"incrementalSyncDescription": "Частота инкрементной синхронизации почты (в минутах)",
"syncScope": "Стратегия синхронизации",
"syncScopeDescription": "Выберите письма для индексации и архивации.",
"selectMode": "Выберите режим фильтрации",
"syncAll": "Синхронизировать все письма",
"sinceFixed": "С определенной даты",
"sinceRelative": "Синхронизировать только новые письма",
"beforeRelative": "Архивировать только старые письма",
"duration": "Продолжительность",
"unit": "Единица",
"accessControl": "Контроль доступа",
"owner": "Создатель",
"access_control": {
"title": "Назначение доступа к аккаунту",
"description": "Назначить роли и авторизованных пользователей для {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "Systemversion"
},
"accounts": {
"beforeRelativeValue": "Synkronisera mejl från före {{value}} {{unit}} sedan",
"sinceRelativeValue": "Synkronisera mejl från de senaste {{value}} {{unit}}",
"syncBatchSize": "Batchstorlek för synk",
"syncBatchSizeDescription": "Antal meddelanden som hämtas per IMAP-förfrågan",
"incrementalSyncDescription": "Hur ofta inkrementell e-post-synkronisering utförs (i minuter)",
"syncScope": "Synkstrategi",
"syncScopeDescription": "Välj vilka e-postmeddelanden som ska indexeras och arkiveras.",
"selectMode": "Välj filterläge",
"syncAll": "Synkronisera alla mejl",
"sinceFixed": "Sedan ett specifikt datum",
"sinceRelative": "Synka endast nyligen inkomna mejl",
"beforeRelative": "Arkivera endast gamla mejl",
"duration": "Varaktighet",
"unit": "Enhet",
"accessControl": "Åtkomstkontroll",
"owner": "Skapad av",
"access_control": {
"title": "Tilldelning av kontotillgång",
"description": "Tilldela roller och auktoriserade användare till {{email}}",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "系統版本"
},
"accounts": {
"beforeRelativeValue": "同步 {{value}} {{unit}} 之前的郵件",
"sinceRelativeValue": "同步最近 {{value}} {{unit}} 內的郵件",
"syncBatchSize": "批次同步數量",
"syncBatchSizeDescription": "每次 IMAP 請求獲取的郵件數量",
"incrementalSyncDescription": "執行增量郵件同步的頻率(分鐘)",
"syncScope": "同步策略",
"syncScopeDescription": "選擇哪些郵件需要被索引和歸檔。",
"selectMode": "選擇過濾模式",
"syncAll": "同步所有郵件",
"sinceFixed": "從特定日期開始 (至今)",
"sinceRelative": "僅同步最近的郵件",
"beforeRelative": "僅封存舊郵件",
"duration": "時長",
"unit": "單位",
"accessControl": "訪問控制",
"owner": "建立者",
"access_control": {
"title": "帳戶訪問分配",
"description": "為 {{email}} 分配角色和授權用戶",
+16
View File
@@ -138,6 +138,22 @@
"systemVersion": "系统版本"
},
"accounts": {
"beforeRelativeValue": "同步 {{value}} {{unit}} 之前的邮件",
"sinceRelativeValue": "同步最近 {{value}} {{unit}} 内的邮件",
"syncBatchSize": "批次同步数量",
"syncBatchSizeDescription": "每次 IMAP 请求获取的邮件数量",
"incrementalSyncDescription": "执行增量邮件同步的频率(分钟)",
"syncScope": "同步策略",
"syncScopeDescription": "选择哪些邮件需要被索引和归档。",
"selectMode": "选择过滤模式",
"syncAll": "同步所有邮件",
"sinceFixed": "从特定日期开始 (至今)",
"sinceRelative": "仅同步最近的邮件 (相对时间)",
"beforeRelative": "仅同步旧邮件",
"duration": "时长",
"unit": "单位",
"accessControl": "访问控制",
"owner": "创建者",
"access_control": {
"title": "账户访问分配",
"description": "为 {{email}} 分配角色和授权用户",