mirror of
https://github.com/LogicLabs-OU/OpenArchiver.git
synced 2026-04-06 00:31:57 +02:00
feat: remove archived emails and related data
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ArchivedEmailService } from '../../services/ArchivedEmailService';
|
||||
import { config } from '../../config';
|
||||
|
||||
export class ArchivedEmailController {
|
||||
public getArchivedEmails = async (req: Request, res: Response): Promise<Response> => {
|
||||
@@ -33,4 +34,23 @@ export class ArchivedEmailController {
|
||||
return res.status(500).json({ message: 'An internal server error occurred' });
|
||||
}
|
||||
};
|
||||
|
||||
public deleteArchivedEmail = async (req: Request, res: Response): Promise<Response> => {
|
||||
if (config.app.isDemo) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ message: 'This operation is not allowed in demo mode.' });
|
||||
}
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await ArchivedEmailService.deleteArchivedEmail(id);
|
||||
return res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error(`Delete archived email ${req.params.id} error:`, error);
|
||||
if (error instanceof Error && error.message === 'Archived email not found') {
|
||||
return res.status(404).json({ message: error.message });
|
||||
}
|
||||
return res.status(500).json({ message: 'An internal server error occurred' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,5 +16,7 @@ export const createArchivedEmailRouter = (
|
||||
|
||||
router.get('/:id', archivedEmailController.getArchivedEmailById);
|
||||
|
||||
router.delete('/:id', archivedEmailController.deleteArchivedEmail);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { count, desc, eq, asc, and } from 'drizzle-orm';
|
||||
import { db } from '../database';
|
||||
import { archivedEmails, attachments, emailAttachments } from '../database/schema';
|
||||
import type { PaginatedArchivedEmails, ArchivedEmail, Recipient, ThreadEmail } from '@open-archiver/types';
|
||||
import type {
|
||||
PaginatedArchivedEmails,
|
||||
ArchivedEmail,
|
||||
Recipient,
|
||||
ThreadEmail,
|
||||
} from '@open-archiver/types';
|
||||
import { StorageService } from './StorageService';
|
||||
import { SearchService } from './SearchService';
|
||||
import type { Readable } from 'stream';
|
||||
|
||||
interface DbRecipients {
|
||||
@@ -139,4 +145,51 @@ export class ArchivedEmailService {
|
||||
|
||||
return mappedEmail;
|
||||
}
|
||||
|
||||
public static async deleteArchivedEmail(emailId: string): Promise<void> {
|
||||
const [email] = await db
|
||||
.select()
|
||||
.from(archivedEmails)
|
||||
.where(eq(archivedEmails.id, emailId));
|
||||
|
||||
if (!email) {
|
||||
throw new Error('Archived email not found');
|
||||
}
|
||||
|
||||
const storage = new StorageService();
|
||||
|
||||
// Load attachments before deleting the email
|
||||
let emailAttachmentsResult: { attachmentId: string; storagePath: string }[] = [];
|
||||
if (email.hasAttachments) {
|
||||
emailAttachmentsResult = await db
|
||||
.select({
|
||||
attachmentId: attachments.id,
|
||||
storagePath: attachments.storagePath,
|
||||
})
|
||||
.from(emailAttachments)
|
||||
.innerJoin(attachments, eq(emailAttachments.attachmentId, attachments.id))
|
||||
.where(eq(emailAttachments.emailId, emailId));
|
||||
}
|
||||
|
||||
// Delete the email file from storage
|
||||
await storage.delete(email.storagePath);
|
||||
|
||||
// Handle attachments: delete only if not referenced elsewhere
|
||||
for (const attachment of emailAttachmentsResult) {
|
||||
const [refCount] = await db
|
||||
.select({ count: count(emailAttachments.emailId) })
|
||||
.from(emailAttachments)
|
||||
.where(eq(emailAttachments.attachmentId, attachment.attachmentId));
|
||||
|
||||
if (refCount.count === 1) {
|
||||
await storage.delete(attachment.storagePath);
|
||||
await db.delete(attachments).where(eq(attachments.id, attachment.attachmentId));
|
||||
}
|
||||
}
|
||||
|
||||
const searchService = new SearchService();
|
||||
await searchService.deleteDocumentsByFilter('emails', `id = ${emailId}`);
|
||||
|
||||
await db.delete(archivedEmails).where(eq(archivedEmails.id, emailId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
import type { PageData } from './$types';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import EmailPreview from '$lib/components/custom/EmailPreview.svelte';
|
||||
import EmailThread from '$lib/components/custom/EmailThread.svelte';
|
||||
import { api } from '$lib/api.client';
|
||||
import { browser } from '$app/environment';
|
||||
import { formatBytes } from '$lib/utils';
|
||||
import EmailPreview from '$lib/components/custom/EmailPreview.svelte';
|
||||
import EmailThread from '$lib/components/custom/EmailThread.svelte';
|
||||
import { api } from '$lib/api.client';
|
||||
import { browser } from '$app/environment';
|
||||
import { formatBytes } from '$lib/utils';
|
||||
import { goto } from '$app/navigation';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let email = $derived(data.email);
|
||||
let { data }: { data: PageData } = $props();
|
||||
let email = $derived(data.email);
|
||||
let isDeleteDialogOpen = $state(false);
|
||||
let isDeleting = $state(false);
|
||||
|
||||
async function download(path: string, filename: string) {
|
||||
if (!browser) return;
|
||||
async function download(path: string, filename: string) {
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
const response = await api(`/storage/download?path=${encodeURIComponent(path)}`);
|
||||
@@ -34,7 +38,26 @@
|
||||
console.error('Download failed:', error);
|
||||
// Optionally, show an error message to the user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!email) return;
|
||||
try {
|
||||
isDeleting = true;
|
||||
const response = await api(`/archived-emails/${email.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete email');
|
||||
}
|
||||
await goto('/dashboard/archived-emails');
|
||||
} catch (error) {
|
||||
console.error('Delete failed:', error);
|
||||
} finally {
|
||||
isDeleting = false;
|
||||
isDeleteDialogOpen = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if email}
|
||||
@@ -112,16 +135,22 @@
|
||||
</Card.Root>
|
||||
</div>
|
||||
<div class="col-span-3 space-y-6 md:col-span-1">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Actions</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<Button onclick={() => download(email.storagePath, `${email.subject || 'email'}.eml`)}
|
||||
>Download Email (.eml)</Button
|
||||
>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Actions</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-2">
|
||||
<Button onclick={() => download(email.storagePath, `${email.subject || 'email'}.eml`)}
|
||||
>Download Email (.eml)</Button
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onclick={() => (isDeleteDialogOpen = true)}
|
||||
>
|
||||
Delete Email
|
||||
</Button>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
{#if email.thread && email.thread.length > 1}
|
||||
<Card.Root>
|
||||
@@ -132,9 +161,34 @@
|
||||
<EmailThread thread={email.thread} currentEmailId={email.id} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Root bind:open={isDeleteDialogOpen}>
|
||||
<Dialog.Content class="sm:max-w-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Are you sure you want to delete this email?</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
This action cannot be undone and will permanently remove the email and its
|
||||
attachments.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Footer class="sm:justify-start">
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onclick={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{#if isDeleting}Deleting...{:else}Confirm{/if}
|
||||
</Button>
|
||||
<Dialog.Close>
|
||||
<Button type="button" variant="secondary">Cancel</Button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
{:else}
|
||||
<p>Email not found.</p>
|
||||
<p>Email not found.</p>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user