mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat: support restoring single message to IMAP #77
This commit is contained in:
@@ -79,12 +79,26 @@ impl ImapExecutor {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn append(
|
||||
&self,
|
||||
mailbox_name: impl AsRef<str>,
|
||||
flags: Option<&str>,
|
||||
internaldate: Option<&str>,
|
||||
content: impl AsRef<[u8]>,
|
||||
) -> BichonResult<()> {
|
||||
let mut session = self.get_connection().await?;
|
||||
session
|
||||
.append(mailbox_name, flags, internaldate, content)
|
||||
.await
|
||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))
|
||||
}
|
||||
|
||||
pub async fn fetch_new_mail(
|
||||
&self,
|
||||
account: &AccountModel,
|
||||
mailbox: &MailBox,
|
||||
start_uid: u64,
|
||||
before: Option<&str>
|
||||
before: Option<&str>,
|
||||
) -> BichonResult<()> {
|
||||
assert!(start_uid > 0, "start_uid must be greater than 0");
|
||||
|
||||
@@ -93,12 +107,7 @@ impl ImapExecutor {
|
||||
None => format!("UID {start_uid}:*"),
|
||||
};
|
||||
|
||||
let uid_list = self
|
||||
.uid_search(
|
||||
&mailbox.encoded_name(),
|
||||
&query,
|
||||
)
|
||||
.await?;
|
||||
let uid_list = self.uid_search(&mailbox.encoded_name(), &query).await?;
|
||||
|
||||
let len = uid_list.len();
|
||||
if len == 0 {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
use crate::{
|
||||
encode_mailbox_name,
|
||||
modules::{
|
||||
account::migration::{AccountModel, AccountType},
|
||||
context::executors::MAIL_CONTEXT,
|
||||
error::{code::ErrorCode, BichonResult},
|
||||
indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER},
|
||||
},
|
||||
raise_error,
|
||||
};
|
||||
use poem_openapi::Object;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const MAX_RESTORE_COUNT: usize = 100;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)]
|
||||
pub struct RestoreMessagesRequest {
|
||||
/// Message IDs to restore (max 100)
|
||||
pub message_ids: Vec<u64>,
|
||||
}
|
||||
|
||||
pub async fn restore_emails(account_id: u64, message_ids: Vec<u64>) -> BichonResult<()> {
|
||||
if message_ids.len() > MAX_RESTORE_COUNT {
|
||||
return Err(raise_error!(
|
||||
format!(
|
||||
"Too many messages to restore: {} (max {})",
|
||||
message_ids.len(),
|
||||
MAX_RESTORE_COUNT
|
||||
),
|
||||
ErrorCode::InvalidParameter
|
||||
));
|
||||
}
|
||||
|
||||
let account = AccountModel::check_account_exists(account_id).await?;
|
||||
if !matches!(account.account_type, AccountType::IMAP) {
|
||||
return Err(raise_error!(
|
||||
"Account type is not IMAP".into(),
|
||||
ErrorCode::Incompatible
|
||||
));
|
||||
}
|
||||
let executor = MAIL_CONTEXT.imap(account.id).await?;
|
||||
|
||||
let mut failed = Vec::new();
|
||||
|
||||
for message_id in message_ids {
|
||||
let result: BichonResult<()> = async {
|
||||
let envelope = ENVELOPE_INDEX_MANAGER
|
||||
.get_envelope_by_id(account_id, message_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
raise_error!(
|
||||
format!(
|
||||
"Envelope not found: account_id={} message_id={}",
|
||||
account_id, message_id
|
||||
),
|
||||
ErrorCode::ResourceNotFound
|
||||
)
|
||||
})?;
|
||||
|
||||
let eml = EML_INDEX_MANAGER
|
||||
.get(account_id, message_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
raise_error!(
|
||||
format!(
|
||||
"Email record not found: account_id={} id={}",
|
||||
account_id, message_id
|
||||
),
|
||||
ErrorCode::ResourceNotFound
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(mailbox_name) = envelope.mailbox_name {
|
||||
executor
|
||||
.append(encode_mailbox_name!(&mailbox_name), None, None, &eml)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Err(err) = result {
|
||||
failed.push(message_id);
|
||||
tracing::warn!(
|
||||
account_id = account_id,
|
||||
message_id = message_id,
|
||||
error = ?err,
|
||||
"Failed to restore email"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !failed.is_empty() {
|
||||
tracing::info!(
|
||||
account_id = account_id,
|
||||
failed_count = failed.len(),
|
||||
failed_message_ids = ?failed,
|
||||
"Restore emails finished with partial failures"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
// 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/>.
|
||||
|
||||
|
||||
pub mod append;
|
||||
pub mod content;
|
||||
pub mod delete;
|
||||
pub mod list;
|
||||
|
||||
@@ -21,6 +21,8 @@ use crate::modules::common::auth::ClientContext;
|
||||
use crate::modules::indexer::envelope::Envelope;
|
||||
use crate::modules::indexer::manager::EML_INDEX_MANAGER;
|
||||
use crate::modules::indexer::manager::ENVELOPE_INDEX_MANAGER;
|
||||
use crate::modules::message::append::restore_emails;
|
||||
use crate::modules::message::append::RestoreMessagesRequest;
|
||||
use crate::modules::message::content::{retrieve_email_content, FullMessageContent};
|
||||
use crate::modules::message::delete::delete_messages_impl;
|
||||
use crate::modules::message::list::{get_thread_messages, list_messages_impl};
|
||||
@@ -227,6 +229,25 @@ impl MessageApi {
|
||||
Ok(attachment)
|
||||
}
|
||||
|
||||
#[oai(
|
||||
path = "/restore-messages/:account_id",
|
||||
method = "post",
|
||||
operation_id = "restore_messages"
|
||||
)]
|
||||
async fn restore_messages(
|
||||
&self,
|
||||
account_id: Path<u64>,
|
||||
/// Message IDs to restore.
|
||||
payload: Json<RestoreMessagesRequest>,
|
||||
context: ClientContext,
|
||||
) -> ApiResult<()> {
|
||||
let account_id = account_id.0;
|
||||
context
|
||||
.require_permission(Some(account_id), Permission::DATA_EXPORT_BATCH)
|
||||
.await?;
|
||||
Ok(restore_emails(account_id, payload.0.message_ids).await?)
|
||||
}
|
||||
|
||||
/// Downloads a specific attachment from an email. Requires `name` query parameter.
|
||||
#[oai(
|
||||
path = "/download-attachment/:account_id/:message_id",
|
||||
|
||||
@@ -97,3 +97,12 @@ export const download_message = async (accountId: number, id: number) => {
|
||||
const blob = new Blob([response.data]);
|
||||
saveAs(blob, `${id}.eml`);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const restore_message = async (accountId: number, messageIds: number[]) => {
|
||||
const response = await axiosInstance.post(`/api/v1/restore-messages/${accountId}`, {
|
||||
message_ids: messageIds,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
open: boolean
|
||||
@@ -79,6 +80,7 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
|
||||
onClick={handleConfirm}
|
||||
disabled={disabled || isLoading}
|
||||
>
|
||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{confirmText ?? t('dialogs.continue')}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import { cn, dateFnsLocaleMap, formatBytes } from "@/lib/utils"
|
||||
import { formatDistanceToNow } from "date-fns"
|
||||
import { MailIcon, Paperclip, Trash2 } from "lucide-react"
|
||||
import { MailIcon, MoreVertical, Paperclip, TagIcon, Trash2 } from "lucide-react"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { EmailEnvelope } from "@/api"
|
||||
import { useMailboxContext } from "../context"
|
||||
@@ -28,6 +28,8 @@ import { MailBulkActions } from "./bulk-actions"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { enUS } from "date-fns/locale"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
interface MailListProps {
|
||||
items: EmailEnvelope[]
|
||||
@@ -179,15 +181,44 @@ export function MailList({
|
||||
{item.date && formatDistanceToNow(new Date(item.date), { addSuffix: true, locale })}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDelete(item)
|
||||
}}
|
||||
className="p-1 rounded hover:bg-destructive/10 hover:text-destructive transition-all"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 p-0 hover:bg-muted rounded-md"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MoreVertical className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end" className="w-44">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelected(new Set([item.id]));
|
||||
setOpen("restore");
|
||||
}}
|
||||
>
|
||||
<TagIcon className="ml-2 h-3.5 w-3.5" />
|
||||
{t('restore_message.restore_to_imap', 'Restore Mail')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(item);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="ml-2 h-3.5 w-3.5" />
|
||||
{t('common.delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,6 +50,7 @@ import { animated, useSpring } from "@react-spring/web"
|
||||
import { TransitionProps } from "@mui/material/transitions"
|
||||
import Collapse from "@mui/material/Collapse"
|
||||
import { FolderIcon } from "lucide-react"
|
||||
import { RestoreMessageDialog } from "./restore-message-dialog"
|
||||
|
||||
|
||||
interface MailProps {
|
||||
@@ -402,6 +403,13 @@ export function Mail({
|
||||
open={open === 'move-to-trash'}
|
||||
onOpenChange={() => setOpen('move-to-trash')}
|
||||
/>
|
||||
|
||||
<RestoreMessageDialog
|
||||
key='envelope-restore'
|
||||
open={open === 'restore'}
|
||||
onOpenChange={() => setOpen('restore')}
|
||||
/>
|
||||
|
||||
</MailboxProvider >
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
|
||||
import { restore_message } from '@/api/mailbox/envelope/api'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { AxiosError } from 'axios'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMailboxContext } from '../context'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
|
||||
interface RestoreMessageDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function RestoreMessageDialog({
|
||||
open,
|
||||
onOpenChange
|
||||
}: RestoreMessageDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const { selectedAccountId, selected, setSelected } = useMailboxContext();
|
||||
|
||||
|
||||
const restoreMutation = useMutation({
|
||||
mutationFn: (messageIds: number[]) =>
|
||||
restore_message(selectedAccountId!, messageIds),
|
||||
onSuccess: handleRestoreSuccess,
|
||||
onError: handleRestoreError,
|
||||
});
|
||||
|
||||
function handleRestoreSuccess() {
|
||||
toast({
|
||||
title: t('restore_message.success', 'Messages restored'),
|
||||
description: t(
|
||||
'restore_message.successDesc',
|
||||
'The selected messages have been restored to the IMAP server.'
|
||||
),
|
||||
action: (
|
||||
<ToastAction altText={t('common.close')}>
|
||||
{t('common.close')}
|
||||
</ToastAction>
|
||||
),
|
||||
});
|
||||
setSelected(new Set());
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
function handleRestoreError(error: AxiosError) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { message?: string })?.message ||
|
||||
error.message ||
|
||||
t('restore_message.failed', 'Failed to restore messages');
|
||||
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t(
|
||||
'restore_message.failedTitle',
|
||||
'Restore failed'
|
||||
),
|
||||
description: errorMessage,
|
||||
action: (
|
||||
<ToastAction altText={t('common.tryAgain')}>
|
||||
{t('common.tryAgain')}
|
||||
</ToastAction>
|
||||
),
|
||||
});
|
||||
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t('restore_message.title', 'Restore messages')}
|
||||
desc={t(
|
||||
'restore_message.desc',
|
||||
'This action will append the selected messages from Bichon to their corresponding mailboxes on the IMAP server.'
|
||||
)}
|
||||
confirmText={t('restore_message.confirm', 'Restore')}
|
||||
handleConfirm={() => restoreMutation.mutate(Array.from(selected))}
|
||||
className="sm:max-w-sm"
|
||||
isLoading={restoreMutation.isPending}
|
||||
disabled={restoreMutation.isPending}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import React from 'react'
|
||||
import { MailboxData } from '@/api/mailbox/api'
|
||||
import { EmailEnvelope } from '@/api'
|
||||
|
||||
export type MailboxDialogType = 'mailbox' | 'display' | 'move-to-trash' | 'filters'
|
||||
export type MailboxDialogType = 'mailbox' | 'display' | 'move-to-trash' | 'filters' | 'restore'
|
||||
|
||||
interface MailboxContextType {
|
||||
open: MailboxDialogType | null
|
||||
|
||||
@@ -25,18 +25,17 @@ import { useState, useEffect } from 'react';
|
||||
import { useAvailableTags } from '@/hooks/use-available-tags';
|
||||
import { useUpdateTags } from '@/hooks/use-update-tags';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { EmailEnvelope } from '@/api';
|
||||
import { validateTag } from '@/lib/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useSearchContext } from './context';
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
currentEnvelope: EmailEnvelope | undefined
|
||||
}
|
||||
|
||||
export function EditTagsDialog({ open, onOpenChange, currentEnvelope }: Props) {
|
||||
export function EditTagsDialog({ open, onOpenChange }: Props) {
|
||||
const { tags: availableTags } = useAvailableTags();
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate, isPending } = useUpdateTags();
|
||||
@@ -45,6 +44,8 @@ export function EditTagsDialog({ open, onOpenChange, currentEnvelope }: Props) {
|
||||
const [commandOpen, setCommandOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { currentEnvelope } = useSearchContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (open && currentEnvelope) {
|
||||
setSelectedTags(currentEnvelope.tags || []);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import React from 'react'
|
||||
import { EmailEnvelope } from '@/api'
|
||||
|
||||
export type SearchDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'search-form'
|
||||
export type SearchDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'search-form' | 'restore'
|
||||
|
||||
interface SearchContextType {
|
||||
open: SearchDialogType | null
|
||||
|
||||
@@ -38,6 +38,7 @@ import { EnvelopeTags } from './tag-facet';
|
||||
import { EditTagsDialog } from './add-tag-dialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Logo from '@/assets/logo.svg'
|
||||
import { RestoreMessageDialog } from './restore-message-dialog';
|
||||
|
||||
export default function Search() {
|
||||
const { t } = useTranslation()
|
||||
@@ -187,7 +188,7 @@ export default function Search() {
|
||||
<EditTagsDialog
|
||||
key='edit-tags-dialog'
|
||||
open={open === 'edit-tags'}
|
||||
onOpenChange={() => setOpen('edit-tags')} currentEnvelope={selectedEnvelope}
|
||||
onOpenChange={() => setOpen('edit-tags')}
|
||||
/>
|
||||
|
||||
<SearchFormDialog
|
||||
@@ -196,6 +197,12 @@ export default function Search() {
|
||||
open={open === 'search-form'}
|
||||
onOpenChange={() => setOpen('search-form')}
|
||||
/>
|
||||
|
||||
<RestoreMessageDialog
|
||||
key='restore-mail-dialog'
|
||||
open={open === 'restore'}
|
||||
onOpenChange={() => setOpen('restore')}
|
||||
/>
|
||||
</SearchProvider>
|
||||
</Main>
|
||||
</>
|
||||
|
||||
@@ -243,7 +243,17 @@ export function MailList({
|
||||
<TagIcon className="ml-2 h-3.5 w-3.5" />
|
||||
{t('search.editTag')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation();
|
||||
setCurrentEnvelope(item);
|
||||
setOpen("restore");
|
||||
}}
|
||||
>
|
||||
<TagIcon className="ml-2 h-3.5 w-3.5" />
|
||||
{t('restore_message.restore_to_imap', 'Restore Mail')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// 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/>.
|
||||
|
||||
|
||||
import { restore_message } from '@/api/mailbox/envelope/api'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { AxiosError } from 'axios'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import { useSearchContext } from './context'
|
||||
|
||||
interface RestoreMessageDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function RestoreMessageDialog({
|
||||
open,
|
||||
onOpenChange
|
||||
}: RestoreMessageDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const { currentEnvelope } = useSearchContext()
|
||||
|
||||
|
||||
const restoreMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
restore_message(currentEnvelope!.account_id, [currentEnvelope!.id]),
|
||||
onSuccess: handleRestoreSuccess,
|
||||
onError: handleRestoreError,
|
||||
});
|
||||
|
||||
function handleRestoreSuccess() {
|
||||
toast({
|
||||
title: t('restore_message.success', 'Messages restored'),
|
||||
description: t(
|
||||
'restore_message.successDesc',
|
||||
'The selected messages have been restored to the IMAP server.'
|
||||
),
|
||||
action: (
|
||||
<ToastAction altText={t('common.close')}>
|
||||
{t('common.close')}
|
||||
</ToastAction>
|
||||
),
|
||||
});
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
function handleRestoreError(error: AxiosError) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { message?: string })?.message ||
|
||||
error.message ||
|
||||
t('restore_message.failed', 'Failed to restore messages');
|
||||
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t(
|
||||
'restore_message.failedTitle',
|
||||
'Restore failed'
|
||||
),
|
||||
description: errorMessage,
|
||||
action: (
|
||||
<ToastAction altText={t('common.tryAgain')}>
|
||||
{t('common.tryAgain')}
|
||||
</ToastAction>
|
||||
),
|
||||
});
|
||||
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t('restore_message.title', 'Restore messages')}
|
||||
desc={t(
|
||||
'restore_message.desc',
|
||||
'This action will append the selected messages from Bichon to their corresponding mailboxes on the IMAP server.'
|
||||
)}
|
||||
confirmText={t('restore_message.confirm', 'Restore')}
|
||||
handleConfirm={() => restoreMutation.mutate()}
|
||||
className="sm:max-w-sm"
|
||||
isLoading={restoreMutation.isPending}
|
||||
disabled={restoreMutation.isPending}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "تسجيل الخروج",
|
||||
"desc": "هل أنت متأكد أنك تريد تسجيل الخروج؟ ستحتاج إلى تسجيل الدخول مرة أخرى للوصول إلى حسابك.",
|
||||
"confirm": "تسجيل الخروج"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "استعادة الرسائل",
|
||||
"desc": "سيقوم هذا الإجراء بإعادة رفع الرسائل المختارة من Bichon واستعادتها إلى صناديق البريد المقابلة لها على خادم IMAP.",
|
||||
"confirm": "تنفيذ الاستعادة",
|
||||
"restore_to_imap": "استعادة البريد",
|
||||
"success": "تمت استعادة الرسائل بنجاح",
|
||||
"successDesc": "تمت استعادة الرسائل المختارة إلى خادم IMAP بنجاح.",
|
||||
"failed": "فشلت استعادة الرسائل",
|
||||
"failedTitle": "فشل الاستعادة"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Log ud",
|
||||
"desc": "Er du sikker på, at du vil logge ud? Du skal logge ind igen for at få adgang til din konto.",
|
||||
"confirm": "Log ud"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Gendan meddelelser",
|
||||
"desc": "Denne handling vil uploade de valgte meddelelser fra Bichon og gendanne dem til deres tilsvarende postkasser på IMAP-serveren.",
|
||||
"confirm": "Gendan",
|
||||
"restore_to_imap": "Gendan e-mail",
|
||||
"success": "Meddelelser gendannet",
|
||||
"successDesc": "De valgte meddelelser er blevet gendannet til IMAP-serveren.",
|
||||
"failed": "Kunne ikke gendanne meddelelser",
|
||||
"failedTitle": "Gendannelse mislykkedes"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Abmelden",
|
||||
"desc": "Möchten Sie sich wirklich abmelden? Sie müssen sich erneut anmelden, um auf Ihr Konto zuzugreifen.",
|
||||
"confirm": "Abmelden"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Nachrichten wiederherstellen",
|
||||
"desc": "Diese Aktion lädt die ausgewählten Nachrichten von Bichon hoch und stellt sie in den entsprechenden Postfächern auf dem IMAP-Server wieder her.",
|
||||
"confirm": "Wiederherstellen",
|
||||
"restore_to_imap": "E-Mail wiederherstellen",
|
||||
"success": "Nachrichten wiederhergestellt",
|
||||
"successDesc": "Die ausgewählten Nachrichten wurden erfolgreich auf dem IMAP-Server wiederhergestellt.",
|
||||
"failed": "Wiederherstellung fehlgeschlagen",
|
||||
"failedTitle": "Fehler bei der Wiederherstellung"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Sign out",
|
||||
"desc": "Are you sure you want to sign out? You will need to sign in again to access your account.",
|
||||
"confirm": "Sign out"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Restore Messages",
|
||||
"desc": "This action will append the selected messages from Bichon back to their corresponding mailboxes on the IMAP server.",
|
||||
"confirm": "Restore",
|
||||
"restore_to_imap": "Restore Mail",
|
||||
"success": "Messages Restored",
|
||||
"successDesc": "The selected messages have been successfully restored to the IMAP server.",
|
||||
"failed": "Failed to restore messages",
|
||||
"failedTitle": "Restore Failed"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Cerrar sesión",
|
||||
"desc": "¿Está seguro de que desea cerrar sesión? Necesitará iniciar sesión nuevamente para acceder a su cuenta.",
|
||||
"confirm": "Cerrar sesión"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Restaurar mensajes",
|
||||
"desc": "Esta acción volverá a cargar los mensajes seleccionados de Bichon y los restaurará en sus carpetas correspondientes en el servidor IMAP.",
|
||||
"confirm": "Restaurar",
|
||||
"restore_to_imap": "Restaurar correo",
|
||||
"success": "Mensajes restaurados",
|
||||
"successDesc": "Los mensajes seleccionados se han restaurado correctamente en el servidor IMAP.",
|
||||
"failed": "Error al restaurar los mensajes",
|
||||
"failedTitle": "Error de restauración"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Kirjaudu ulos",
|
||||
"desc": "Haluatko varmasti kirjautua ulos? Sinun täytyy kirjautua uudelleen päästäksesi tilillesi.",
|
||||
"confirm": "Kirjaudu ulos"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Palauta viestit",
|
||||
"desc": "Tämä toiminto lataa valitut viestit Bichonista ja palauttaa ne vastaaviin postilaatikoihin IMAP-palvelimella.",
|
||||
"confirm": "Palauta",
|
||||
"restore_to_imap": "Palauta sähköposti",
|
||||
"success": "Viestit palautettu",
|
||||
"successDesc": "Valitut viestit on palautettu onnistuneesti IMAP-palvelimelle.",
|
||||
"failed": "Viestien palautus epäonnistui",
|
||||
"failedTitle": "Palautus epäonnistui"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Déconnexion",
|
||||
"desc": "Êtes-vous sûr de vouloir vous déconnecter ? Vous devrez vous reconnecter pour accéder à votre compte.",
|
||||
"confirm": "Déconnexion"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Restaurer les messages",
|
||||
"desc": "Cette action téléchargera les messages sélectionnés depuis Bichon et les restaurera dans leurs boîtes aux lettres correspondantes sur le serveur IMAP.",
|
||||
"confirm": "Restaurer",
|
||||
"restore_to_imap": "Restaurer le courrier",
|
||||
"success": "Messages restaurés",
|
||||
"successDesc": "Les messages sélectionnés ont été restaurés avec succès sur le serveur IMAP.",
|
||||
"failed": "Échec de la restauration des messages",
|
||||
"failedTitle": "Échec de la restauration"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Disconnetti",
|
||||
"desc": "Sei sicuro di voler disconnetterti? Dovrai accedere di nuovo per usare il tuo account.",
|
||||
"confirm": "Disconnetti"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Ripristina messaggi",
|
||||
"desc": "Questa azione caricherà i messaggi selezionati da Bichon e li ripristinerà nelle rispettive caselle di posta sul server IMAP.",
|
||||
"confirm": "Ripristina",
|
||||
"restore_to_imap": "Ripristina posta",
|
||||
"success": "Messaggi ripristinati",
|
||||
"successDesc": "I messaggi selezionati sono stati ripristinati con successo sul server IMAP.",
|
||||
"failed": "Impossibile ripristinare i messaggi",
|
||||
"failedTitle": "Ripristino fallito"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "サインアウト",
|
||||
"desc": "本当にサインアウトしますか? アカウントにアクセスするには再度サインインが必要です。",
|
||||
"confirm": "サインアウト"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "メッセージを復元",
|
||||
"desc": "この操作により、選択したメッセージをBichonから再アップロードし、IMAPサーバー上の対応するメールボックスに復元します。",
|
||||
"confirm": "復元を実行",
|
||||
"restore_to_imap": "メールを復元",
|
||||
"success": "メッセージを復元しました",
|
||||
"successDesc": "選択したメッセージがIMAPサーバーに正常に復元されました。",
|
||||
"failed": "メッセージの復元に失敗しました",
|
||||
"failedTitle": "復元失敗"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "로그아웃",
|
||||
"desc": "로그아웃하시겠습니까? 계정에 접근하려면 다시 로그인해야 합니다.",
|
||||
"confirm": "로그아웃"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "메시지 복원",
|
||||
"desc": "이 작업은 선택한 메시지를 Bichon에서 다시 업로드하여 IMAP 서버의 해당 사서함으로 복원합니다.",
|
||||
"confirm": "복원 실행",
|
||||
"restore_to_imap": "메일 복원",
|
||||
"success": "메시지 복원 완료",
|
||||
"successDesc": "선택한 메시지가 IMAP 서버로 성공적으로 복원되었습니다.",
|
||||
"failed": "메시지 복원 실패",
|
||||
"failedTitle": "복원 실패"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Uitloggen",
|
||||
"desc": "Weet je zeker dat je wilt uitloggen? Je moet opnieuw inloggen om toegang te krijgen tot je account.",
|
||||
"confirm": "Uitloggen"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Berichten herstellen",
|
||||
"desc": "Deze actie uploadt de geselecteerde berichten van Bichon en herstelt ze in de bijbehorende mailboxen op de IMAP-server.",
|
||||
"confirm": "Herstellen",
|
||||
"restore_to_imap": "E-mail herstellen",
|
||||
"success": "Berichten hersteld",
|
||||
"successDesc": "De geselecteerde berichten zijn succesvol hersteld op de IMAP-server.",
|
||||
"failed": "Herstellen van berichten mislukt",
|
||||
"failedTitle": "Herstel mislukt"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Logg ut",
|
||||
"desc": "Er du sikker på at du vil logge ut? Du må logge inn igjen for å få tilgang til kontoen din.",
|
||||
"confirm": "Logg ut"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Gjenopprett meldinger",
|
||||
"desc": "Denne handlingen vil laste opp de valgte meldingene fra Bichon og gjenopprette dem til deres tilsvarende postbokser på IMAP-serveren.",
|
||||
"confirm": "Gjenopprett",
|
||||
"restore_to_imap": "Gjenopprett e-post",
|
||||
"success": "Meldinger gjenopprettet",
|
||||
"successDesc": "De valgte meldingene har blitt gjenopprettet til IMAP-serveren.",
|
||||
"failed": "Kunne ikke gjenopprette meldinger",
|
||||
"failedTitle": "Gjenoppretting mislyktes"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Wyloguj się",
|
||||
"desc": "Czy na pewno chcesz się wylogować? Aby uzyskać dostęp do konta, będziesz musiał zalogować się ponownie.",
|
||||
"confirm": "Wyloguj się"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Przywróć wiadomości",
|
||||
"desc": "Ta operacja prześle wybrane wiadomości z Bichon i przywróci je do odpowiednich skrzynek pocztowych na serwerze IMAP.",
|
||||
"confirm": "Przywróć",
|
||||
"restore_to_imap": "Przywróć pocztę",
|
||||
"success": "Wiadomości przywrócone",
|
||||
"successDesc": "Wybrane wiadomości zostały pomyślnie przywrócone na serwer IMAP.",
|
||||
"failed": "Nie udało się przywrócić wiadomości",
|
||||
"failedTitle": "Przywracanie nie powiodło się"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Sair",
|
||||
"desc": "Tem certeza de que deseja sair? Você precisará entrar novamente para acessar sua conta.",
|
||||
"confirm": "Sair"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Restaurar mensagens",
|
||||
"desc": "Esta ação irá carregar as mensagens selecionadas do Bichon e restaurá-las nas respetivas caixas de correio no servidor IMAP.",
|
||||
"confirm": "Restaurar",
|
||||
"restore_to_imap": "Restaurar e-mail",
|
||||
"success": "Mensagens restauradas",
|
||||
"successDesc": "As mensagens selecionadas foram restauradas com sucesso para o servidor IMAP.",
|
||||
"failed": "Falha ao restaurar mensagens",
|
||||
"failedTitle": "Falha na restauração"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Выйти",
|
||||
"desc": "Вы уверены, что хотите выйти? Вам нужно будет снова войти, чтобы получить доступ к аккаунту.",
|
||||
"confirm": "Выйти"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Восстановить сообщения",
|
||||
"desc": "Это действие загрузит выбранные сообщения из Bichon и восстановит их в соответствующих почтовых ящиках на IMAP-сервере.",
|
||||
"confirm": "Восстановить",
|
||||
"restore_to_imap": "Восстановить почту",
|
||||
"success": "Сообщения восстановлены",
|
||||
"successDesc": "Выбранные сообщения были успешно восстановлены на IMAP-сервере.",
|
||||
"failed": "Не удалось восстановить сообщения",
|
||||
"failedTitle": "Ошибка восстановления"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "Logga ut",
|
||||
"desc": "Är du säker på att du vill logga ut? Du måste logga in igen för att få åtkomst till ditt konto.",
|
||||
"confirm": "Logga ut"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "Återställ meddelanden",
|
||||
"desc": "Denna åtgärd kommer att ladda upp de valda meddelandena från Bichon och återställa dem till deras motsvarande brevlådor på IMAP-servern.",
|
||||
"confirm": "Återställ",
|
||||
"restore_to_imap": "Återställ e-post",
|
||||
"success": "Meddelanden återställda",
|
||||
"successDesc": "De valda meddelandena har återställts till IMAP-servern.",
|
||||
"failed": "Misslyckades med att återställa meddelanden",
|
||||
"failedTitle": "Återställning misslyckades"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "登出",
|
||||
"desc": "您確定要登出嗎?您需要重新登入才能存取帳戶。",
|
||||
"confirm": "登出"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "還原郵件",
|
||||
"desc": "此操作將把選定的郵件從 Bichon 系統重新上傳並還原到其在 IMAP 伺服器上對應的信箱中。",
|
||||
"confirm": "執行還原",
|
||||
"restore_to_imap": "還原郵件",
|
||||
"success": "郵件還原成功",
|
||||
"successDesc": "選定的郵件已成功還原到 IMAP 伺服器。",
|
||||
"failed": "還原郵件失敗",
|
||||
"failedTitle": "還原失敗"
|
||||
}
|
||||
}
|
||||
@@ -1439,5 +1439,15 @@
|
||||
"title": "退出登录",
|
||||
"desc": "您确定要退出登录吗?您需要重新登录才能访问账户。",
|
||||
"confirm": "退出登录"
|
||||
},
|
||||
"restore_message": {
|
||||
"title": "还原邮件",
|
||||
"desc": "此操作将把选定的邮件从 Bichon 系统重新上传并恢复到其在 IMAP 服务器上对应的邮箱中。",
|
||||
"confirm": "执行还原",
|
||||
"restore_to_imap": "还原邮件",
|
||||
"success": "邮件还原成功",
|
||||
"successDesc": "选定的邮件已成功恢复到 IMAP 服务器。",
|
||||
"failed": "还原邮件失败",
|
||||
"failedTitle": "还原失败"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user