added backup and email client
This commit is contained in:
@@ -0,0 +1,957 @@
|
||||
// ==================== CACHED EMAIL SERVICE ====================
|
||||
// Service für E-Mail-Caching und Vertragszuordnung
|
||||
|
||||
import { PrismaClient, CachedEmail, Prisma, EmailFolder } from '@prisma/client';
|
||||
import { decrypt } from '../utils/encryption.js';
|
||||
import { fetchEmails, ImapCredentials, FetchedEmail, moveToTrash, restoreFromTrash, permanentDelete } from './imapService.js';
|
||||
import { getImapSmtpSettings } from './emailProvider/emailProviderService.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// ==================== TYPES ====================
|
||||
|
||||
export interface CachedEmailWithRelations extends CachedEmail {
|
||||
stressfreiEmail?: {
|
||||
id: number;
|
||||
email: string;
|
||||
customerId: number;
|
||||
};
|
||||
contract?: {
|
||||
id: number;
|
||||
contractNumber: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
// Parameter für gesendete E-Mail
|
||||
export interface SentEmailParams {
|
||||
to: string[];
|
||||
cc?: string[];
|
||||
subject: string;
|
||||
text?: string;
|
||||
html?: string;
|
||||
messageId: string;
|
||||
contractId?: number; // Optional: Vertrag dem die E-Mail zugeordnet wird
|
||||
attachmentNames?: string[]; // Namen der Anhänge
|
||||
uid?: number; // UID im IMAP Sent-Ordner (für Attachment-Download)
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
success: boolean;
|
||||
newEmails: number;
|
||||
totalEmails: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface EmailListOptions {
|
||||
stressfreiEmailId?: number;
|
||||
customerId?: number;
|
||||
contractId?: number;
|
||||
folder?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
includeBody?: boolean;
|
||||
}
|
||||
|
||||
// ==================== SYNC FUNCTIONS ====================
|
||||
|
||||
// E-Mails für eine StressfreiEmail synchronisieren
|
||||
export async function syncEmailsForAccount(
|
||||
stressfreiEmailId: number,
|
||||
options?: { folder?: string; fullSync?: boolean }
|
||||
): Promise<SyncResult> {
|
||||
const folder = options?.folder || 'INBOX';
|
||||
const fullSync = options?.fullSync || false;
|
||||
|
||||
try {
|
||||
// StressfreiEmail mit Mailbox-Daten laden
|
||||
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||
where: { id: stressfreiEmailId },
|
||||
});
|
||||
|
||||
if (!stressfreiEmail) {
|
||||
return { success: false, newEmails: 0, totalEmails: 0, error: 'StressfreiEmail nicht gefunden' };
|
||||
}
|
||||
|
||||
if (!stressfreiEmail.hasMailbox || !stressfreiEmail.emailPasswordEncrypted) {
|
||||
return { success: false, newEmails: 0, totalEmails: 0, error: 'Keine Mailbox für diese E-Mail-Adresse' };
|
||||
}
|
||||
|
||||
// IMAP/SMTP-Einstellungen vom Provider holen
|
||||
const settings = await getImapSmtpSettings();
|
||||
if (!settings) {
|
||||
return { success: false, newEmails: 0, totalEmails: 0, error: 'Keine E-Mail-Provider-Einstellungen gefunden' };
|
||||
}
|
||||
|
||||
// Passwort entschlüsseln
|
||||
let password: string;
|
||||
try {
|
||||
password = decrypt(stressfreiEmail.emailPasswordEncrypted);
|
||||
} catch {
|
||||
return { success: false, newEmails: 0, totalEmails: 0, error: 'Passwort konnte nicht entschlüsselt werden' };
|
||||
}
|
||||
|
||||
// IMAP-Credentials zusammenstellen
|
||||
const credentials: ImapCredentials = {
|
||||
host: settings.imapServer,
|
||||
port: settings.imapPort,
|
||||
user: stressfreiEmail.email,
|
||||
password,
|
||||
encryption: settings.imapEncryption,
|
||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||
};
|
||||
|
||||
// Folder-Mapping: IMAP-Ordner zu DB-Ordner
|
||||
const dbFolder = folder.toUpperCase() === 'SENT' || folder.toLowerCase() === 'sent'
|
||||
? EmailFolder.SENT
|
||||
: EmailFolder.INBOX;
|
||||
|
||||
// Für inkrementellen Sync: Höchste UID des Ordners ermitteln
|
||||
let sinceUid: number | undefined;
|
||||
if (!fullSync) {
|
||||
const lastEmail = await prisma.cachedEmail.findFirst({
|
||||
where: { stressfreiEmailId, folder: dbFolder },
|
||||
orderBy: { uid: 'desc' },
|
||||
select: { uid: true },
|
||||
});
|
||||
if (lastEmail) {
|
||||
sinceUid = lastEmail.uid;
|
||||
}
|
||||
}
|
||||
|
||||
// E-Mails vom IMAP-Server abrufen
|
||||
const fetchedEmails = await fetchEmails(credentials, {
|
||||
folder,
|
||||
sinceUid,
|
||||
limit: fullSync ? 500 : 100, // Mehr bei Full-Sync
|
||||
});
|
||||
|
||||
// Neue E-Mails in DB speichern
|
||||
let newCount = 0;
|
||||
for (const email of fetchedEmails) {
|
||||
const created = await upsertCachedEmail(stressfreiEmailId, email, dbFolder);
|
||||
if (created) newCount++;
|
||||
}
|
||||
|
||||
// Gesamtzahl ermitteln
|
||||
const totalEmails = await prisma.cachedEmail.count({
|
||||
where: { stressfreiEmailId },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newEmails: newCount,
|
||||
totalEmails,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('syncEmailsForAccount error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
return {
|
||||
success: false,
|
||||
newEmails: 0,
|
||||
totalEmails: 0,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Alle Ordner synchronisieren (INBOX + SENT)
|
||||
export async function syncAllFoldersForAccount(
|
||||
stressfreiEmailId: number,
|
||||
options?: { fullSync?: boolean }
|
||||
): Promise<SyncResult> {
|
||||
const fullSync = options?.fullSync || false;
|
||||
|
||||
// INBOX synchronisieren
|
||||
const inboxResult = await syncEmailsForAccount(stressfreiEmailId, { folder: 'INBOX', fullSync });
|
||||
|
||||
// SENT synchronisieren (Plesk verwendet "Sent" als Ordnername)
|
||||
const sentResult = await syncEmailsForAccount(stressfreiEmailId, { folder: 'Sent', fullSync });
|
||||
|
||||
// Ergebnisse kombinieren
|
||||
if (!inboxResult.success && !sentResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
newEmails: 0,
|
||||
totalEmails: 0,
|
||||
error: inboxResult.error || sentResult.error || 'Synchronisation fehlgeschlagen',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newEmails: inboxResult.newEmails + sentResult.newEmails,
|
||||
totalEmails: inboxResult.totalEmails,
|
||||
};
|
||||
}
|
||||
|
||||
// Einzelne E-Mail in DB speichern/aktualisieren
|
||||
async function upsertCachedEmail(
|
||||
stressfreiEmailId: number,
|
||||
email: FetchedEmail,
|
||||
folder: EmailFolder = EmailFolder.INBOX
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Prüfen ob bereits vorhanden (via messageId + folder)
|
||||
// WICHTIG: Gleiche messageId kann in INBOX und SENT existieren (z.B. E-Mail an sich selbst)
|
||||
const existing = await prisma.cachedEmail.findUnique({
|
||||
where: {
|
||||
stressfreiEmailId_messageId_folder: {
|
||||
stressfreiEmailId,
|
||||
messageId: email.messageId,
|
||||
folder,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Bereits vorhanden
|
||||
return false;
|
||||
}
|
||||
|
||||
// Neue E-Mail anlegen
|
||||
await prisma.cachedEmail.create({
|
||||
data: {
|
||||
stressfreiEmailId,
|
||||
folder,
|
||||
messageId: email.messageId,
|
||||
uid: email.uid,
|
||||
subject: email.subject,
|
||||
fromAddress: email.fromAddress,
|
||||
fromName: email.fromName,
|
||||
toAddresses: JSON.stringify(email.toAddresses),
|
||||
ccAddresses: email.ccAddresses.length > 0 ? JSON.stringify(email.ccAddresses) : null,
|
||||
receivedAt: email.date,
|
||||
textBody: email.textBody,
|
||||
htmlBody: email.htmlBody,
|
||||
hasAttachments: email.hasAttachments,
|
||||
attachmentNames: email.attachmentNames.length > 0
|
||||
? JSON.stringify(email.attachmentNames)
|
||||
: null,
|
||||
isRead: folder === EmailFolder.SENT, // Gesendete E-Mails sind bereits gelesen
|
||||
isStarred: false,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Duplikat-Fehler ignorieren (Race Condition)
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CRUD FUNCTIONS ====================
|
||||
|
||||
// E-Mails abrufen mit Optionen
|
||||
export async function getCachedEmails(
|
||||
options: EmailListOptions
|
||||
): Promise<CachedEmailWithRelations[]> {
|
||||
const where: Prisma.CachedEmailWhereInput = {
|
||||
isDeleted: false, // Gelöschte E-Mails ausschließen
|
||||
};
|
||||
|
||||
if (options.stressfreiEmailId) {
|
||||
where.stressfreiEmailId = options.stressfreiEmailId;
|
||||
}
|
||||
|
||||
if (options.customerId) {
|
||||
where.stressfreiEmail = {
|
||||
customerId: options.customerId,
|
||||
};
|
||||
}
|
||||
|
||||
if (options.contractId) {
|
||||
where.contractId = options.contractId;
|
||||
}
|
||||
|
||||
// Folder-Filter (INBOX oder SENT)
|
||||
if (options.folder === 'SENT') {
|
||||
where.folder = EmailFolder.SENT;
|
||||
} else if (options.folder === 'INBOX' || !options.folder) {
|
||||
// Standard: Nur Posteingang
|
||||
where.folder = EmailFolder.INBOX;
|
||||
}
|
||||
|
||||
// Body-Felder nur wenn explizit angefordert (spart Bandbreite)
|
||||
const select: Prisma.CachedEmailSelect = {
|
||||
id: true,
|
||||
stressfreiEmailId: true,
|
||||
folder: true,
|
||||
messageId: true,
|
||||
uid: true,
|
||||
subject: true,
|
||||
fromAddress: true,
|
||||
fromName: true,
|
||||
toAddresses: true,
|
||||
ccAddresses: true,
|
||||
receivedAt: true,
|
||||
hasAttachments: true,
|
||||
attachmentNames: true,
|
||||
contractId: true,
|
||||
assignedAt: true,
|
||||
assignedBy: true,
|
||||
isAutoAssigned: true,
|
||||
isRead: true,
|
||||
isStarred: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
stressfreiEmail: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
customerId: true,
|
||||
},
|
||||
},
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (options.includeBody) {
|
||||
select.textBody = true;
|
||||
select.htmlBody = true;
|
||||
}
|
||||
|
||||
const emails = await prisma.cachedEmail.findMany({
|
||||
where,
|
||||
select,
|
||||
orderBy: { receivedAt: 'desc' },
|
||||
take: options.limit || 50,
|
||||
skip: options.offset || 0,
|
||||
});
|
||||
|
||||
return emails as CachedEmailWithRelations[];
|
||||
}
|
||||
|
||||
// Einzelne E-Mail abrufen (mit Body)
|
||||
export async function getCachedEmailById(
|
||||
id: number
|
||||
): Promise<CachedEmailWithRelations | null> {
|
||||
const email = await prisma.cachedEmail.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
stressfreiEmail: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
customerId: true,
|
||||
},
|
||||
},
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return email as CachedEmailWithRelations | null;
|
||||
}
|
||||
|
||||
// E-Mail als gelesen markieren
|
||||
export async function markEmailAsRead(id: number): Promise<void> {
|
||||
await prisma.cachedEmail.update({
|
||||
where: { id },
|
||||
data: { isRead: true },
|
||||
});
|
||||
}
|
||||
|
||||
// E-Mail als ungelesen markieren
|
||||
export async function markEmailAsUnread(id: number): Promise<void> {
|
||||
await prisma.cachedEmail.update({
|
||||
where: { id },
|
||||
data: { isRead: false },
|
||||
});
|
||||
}
|
||||
|
||||
// E-Mail Stern setzen/entfernen
|
||||
export async function toggleEmailStar(id: number): Promise<boolean> {
|
||||
const email = await prisma.cachedEmail.findUnique({
|
||||
where: { id },
|
||||
select: { isStarred: true },
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new Error('E-Mail nicht gefunden');
|
||||
}
|
||||
|
||||
const newValue = !email.isStarred;
|
||||
await prisma.cachedEmail.update({
|
||||
where: { id },
|
||||
data: { isStarred: newValue },
|
||||
});
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
// ==================== CONTRACT ASSIGNMENT ====================
|
||||
|
||||
// E-Mail einem Vertrag zuordnen
|
||||
export async function assignEmailToContract(
|
||||
emailId: number,
|
||||
contractId: number,
|
||||
userId?: number
|
||||
): Promise<CachedEmailWithRelations> {
|
||||
// Prüfen ob E-Mail existiert
|
||||
const email = await prisma.cachedEmail.findUnique({
|
||||
where: { id: emailId },
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new Error('E-Mail nicht gefunden');
|
||||
}
|
||||
|
||||
// Prüfen ob Vertrag existiert
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id: contractId },
|
||||
});
|
||||
|
||||
if (!contract) {
|
||||
throw new Error('Vertrag nicht gefunden');
|
||||
}
|
||||
|
||||
// Zuordnung setzen (manuell)
|
||||
const updated = await prisma.cachedEmail.update({
|
||||
where: { id: emailId },
|
||||
data: {
|
||||
contractId,
|
||||
assignedAt: new Date(),
|
||||
assignedBy: userId || null,
|
||||
isAutoAssigned: false, // Manuell zugeordnet
|
||||
},
|
||||
include: {
|
||||
stressfreiEmail: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
customerId: true,
|
||||
},
|
||||
},
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return updated as CachedEmailWithRelations;
|
||||
}
|
||||
|
||||
// Vertragszuordnung aufheben
|
||||
export async function unassignEmailFromContract(
|
||||
emailId: number
|
||||
): Promise<CachedEmailWithRelations> {
|
||||
const updated = await prisma.cachedEmail.update({
|
||||
where: { id: emailId },
|
||||
data: {
|
||||
contractId: null,
|
||||
assignedAt: null,
|
||||
assignedBy: null,
|
||||
},
|
||||
include: {
|
||||
stressfreiEmail: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
customerId: true,
|
||||
},
|
||||
},
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return updated as CachedEmailWithRelations;
|
||||
}
|
||||
|
||||
// ==================== HELPER FUNCTIONS ====================
|
||||
|
||||
// Anzahl ungelesener E-Mails für Kunde
|
||||
export async function getUnreadCountForCustomer(customerId: number): Promise<number> {
|
||||
return prisma.cachedEmail.count({
|
||||
where: {
|
||||
stressfreiEmail: {
|
||||
customerId,
|
||||
},
|
||||
isRead: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Anzahl ungelesener E-Mails für Vertrag
|
||||
export async function getUnreadCountForContract(contractId: number): Promise<number> {
|
||||
return prisma.cachedEmail.count({
|
||||
where: {
|
||||
contractId,
|
||||
isRead: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// E-Mail-Anzahl pro Ordner für ein Konto (total und ungelesen)
|
||||
export async function getFolderCountsForAccount(stressfreiEmailId: number): Promise<{
|
||||
inbox: number;
|
||||
inboxUnread: number;
|
||||
sent: number;
|
||||
sentUnread: number;
|
||||
trash: number;
|
||||
trashUnread: number;
|
||||
}> {
|
||||
const [inbox, inboxUnread, sent, sentUnread, trash, trashUnread] = await Promise.all([
|
||||
// INBOX total
|
||||
prisma.cachedEmail.count({
|
||||
where: { stressfreiEmailId, folder: EmailFolder.INBOX, isDeleted: false },
|
||||
}),
|
||||
// INBOX unread
|
||||
prisma.cachedEmail.count({
|
||||
where: { stressfreiEmailId, folder: EmailFolder.INBOX, isDeleted: false, isRead: false },
|
||||
}),
|
||||
// SENT total
|
||||
prisma.cachedEmail.count({
|
||||
where: { stressfreiEmailId, folder: EmailFolder.SENT, isDeleted: false },
|
||||
}),
|
||||
// SENT unread
|
||||
prisma.cachedEmail.count({
|
||||
where: { stressfreiEmailId, folder: EmailFolder.SENT, isDeleted: false, isRead: false },
|
||||
}),
|
||||
// TRASH total (isDeleted = true)
|
||||
prisma.cachedEmail.count({
|
||||
where: { stressfreiEmailId, isDeleted: true },
|
||||
}),
|
||||
// TRASH unread
|
||||
prisma.cachedEmail.count({
|
||||
where: { stressfreiEmailId, isDeleted: true, isRead: false },
|
||||
}),
|
||||
]);
|
||||
|
||||
return { inbox, inboxUnread, sent, sentUnread, trash, trashUnread };
|
||||
}
|
||||
|
||||
// E-Mail-Anzahl pro Ordner für einen Vertrag (zugeordnete E-Mails)
|
||||
export async function getFolderCountsForContract(contractId: number): Promise<{
|
||||
inbox: number;
|
||||
inboxUnread: number;
|
||||
sent: number;
|
||||
sentUnread: number;
|
||||
}> {
|
||||
const [inbox, inboxUnread, sent, sentUnread] = await Promise.all([
|
||||
// INBOX total
|
||||
prisma.cachedEmail.count({
|
||||
where: { contractId, folder: EmailFolder.INBOX, isDeleted: false },
|
||||
}),
|
||||
// INBOX unread
|
||||
prisma.cachedEmail.count({
|
||||
where: { contractId, folder: EmailFolder.INBOX, isDeleted: false, isRead: false },
|
||||
}),
|
||||
// SENT total
|
||||
prisma.cachedEmail.count({
|
||||
where: { contractId, folder: EmailFolder.SENT, isDeleted: false },
|
||||
}),
|
||||
// SENT unread
|
||||
prisma.cachedEmail.count({
|
||||
where: { contractId, folder: EmailFolder.SENT, isDeleted: false, isRead: false },
|
||||
}),
|
||||
]);
|
||||
|
||||
return { inbox, inboxUnread, sent, sentUnread };
|
||||
}
|
||||
|
||||
// Alle StressfreiEmails eines Kunden mit Mailbox
|
||||
export async function getMailboxAccountsForCustomer(customerId: number) {
|
||||
return prisma.stressfreiEmail.findMany({
|
||||
where: {
|
||||
customerId,
|
||||
hasMailbox: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
notes: true,
|
||||
_count: {
|
||||
select: {
|
||||
cachedEmails: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { email: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
// E-Mail-Thread finden (basierend auf References/In-Reply-To)
|
||||
export async function getEmailThread(emailId: number): Promise<CachedEmailWithRelations[]> {
|
||||
const email = await prisma.cachedEmail.findUnique({
|
||||
where: { id: emailId },
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Suche nach E-Mails mit dem gleichen Betreff (vereinfachte Thread-Logik)
|
||||
// In einer vollständigen Implementierung würde man References/In-Reply-To parsen
|
||||
const baseSubject = email.subject?.replace(/^(Re|Fwd|Aw|Wg):\s*/gi, '') || '';
|
||||
|
||||
if (!baseSubject) {
|
||||
return [email as CachedEmailWithRelations];
|
||||
}
|
||||
|
||||
const thread = await prisma.cachedEmail.findMany({
|
||||
where: {
|
||||
stressfreiEmailId: email.stressfreiEmailId,
|
||||
OR: [
|
||||
{ subject: baseSubject },
|
||||
{ subject: { startsWith: `Re: ${baseSubject}` } },
|
||||
{ subject: { startsWith: `Aw: ${baseSubject}` } },
|
||||
{ subject: { startsWith: `Fwd: ${baseSubject}` } },
|
||||
{ subject: { startsWith: `Wg: ${baseSubject}` } },
|
||||
],
|
||||
},
|
||||
include: {
|
||||
stressfreiEmail: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
customerId: true,
|
||||
},
|
||||
},
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { receivedAt: 'asc' },
|
||||
});
|
||||
|
||||
return thread as CachedEmailWithRelations[];
|
||||
}
|
||||
|
||||
// ==================== TRASH OPERATIONS ====================
|
||||
|
||||
export interface TrashOperationResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// E-Mail in Papierkorb verschieben (markiert als gelöscht + IMAP-Move)
|
||||
export async function moveEmailToTrash(id: number): Promise<TrashOperationResult> {
|
||||
// E-Mail mit StressfreiEmail laden
|
||||
const email = await prisma.cachedEmail.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
stressfreiEmail: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
return { success: false, error: 'E-Mail nicht gefunden' };
|
||||
}
|
||||
|
||||
if (email.isDeleted) {
|
||||
return { success: false, error: 'E-Mail ist bereits im Papierkorb' };
|
||||
}
|
||||
|
||||
// IMAP-Einstellungen und Credentials laden
|
||||
const settings = await getImapSmtpSettings();
|
||||
if (!settings) {
|
||||
return { success: false, error: 'Keine E-Mail-Provider-Einstellungen' };
|
||||
}
|
||||
|
||||
if (!email.stressfreiEmail.emailPasswordEncrypted) {
|
||||
return { success: false, error: 'Keine Mailbox-Zugangsdaten' };
|
||||
}
|
||||
|
||||
let password: string;
|
||||
try {
|
||||
password = decrypt(email.stressfreiEmail.emailPasswordEncrypted);
|
||||
} catch {
|
||||
return { success: false, error: 'Passwort konnte nicht entschlüsselt werden' };
|
||||
}
|
||||
|
||||
const credentials: ImapCredentials = {
|
||||
host: settings.imapServer,
|
||||
port: settings.imapPort,
|
||||
user: email.stressfreiEmail.email,
|
||||
password,
|
||||
encryption: settings.imapEncryption,
|
||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||
};
|
||||
|
||||
// Ordner bestimmen (INBOX oder Sent)
|
||||
const sourceFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX';
|
||||
|
||||
// Auf IMAP-Server in Trash verschieben
|
||||
const imapResult = await moveToTrash(credentials, email.uid, sourceFolder);
|
||||
|
||||
if (!imapResult.success) {
|
||||
return { success: false, error: imapResult.error || 'IMAP-Fehler beim Verschieben' };
|
||||
}
|
||||
|
||||
// In DB als gelöscht markieren (mit neuer UID im Trash)
|
||||
await prisma.cachedEmail.update({
|
||||
where: { id },
|
||||
data: {
|
||||
isDeleted: true,
|
||||
deletedAt: new Date(),
|
||||
uid: imapResult.newUid || email.uid, // Neue UID im Trash speichern
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// E-Mail aus Papierkorb wiederherstellen
|
||||
export async function restoreEmailFromTrash(id: number): Promise<TrashOperationResult> {
|
||||
// E-Mail laden
|
||||
const email = await prisma.cachedEmail.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
stressfreiEmail: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
return { success: false, error: 'E-Mail nicht gefunden' };
|
||||
}
|
||||
|
||||
if (!email.isDeleted) {
|
||||
return { success: false, error: 'E-Mail ist nicht im Papierkorb' };
|
||||
}
|
||||
|
||||
// IMAP-Einstellungen und Credentials
|
||||
const settings = await getImapSmtpSettings();
|
||||
if (!settings) {
|
||||
return { success: false, error: 'Keine E-Mail-Provider-Einstellungen' };
|
||||
}
|
||||
|
||||
if (!email.stressfreiEmail.emailPasswordEncrypted) {
|
||||
return { success: false, error: 'Keine Mailbox-Zugangsdaten' };
|
||||
}
|
||||
|
||||
let password: string;
|
||||
try {
|
||||
password = decrypt(email.stressfreiEmail.emailPasswordEncrypted);
|
||||
} catch {
|
||||
return { success: false, error: 'Passwort konnte nicht entschlüsselt werden' };
|
||||
}
|
||||
|
||||
const credentials: ImapCredentials = {
|
||||
host: settings.imapServer,
|
||||
port: settings.imapPort,
|
||||
user: email.stressfreiEmail.email,
|
||||
password,
|
||||
encryption: settings.imapEncryption,
|
||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||
};
|
||||
|
||||
// Ziel-Ordner bestimmen (basierend auf originalem Ordner)
|
||||
const targetFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX';
|
||||
|
||||
// Auf IMAP-Server aus Trash wiederherstellen
|
||||
const imapResult = await restoreFromTrash(credentials, email.uid, targetFolder);
|
||||
|
||||
if (!imapResult.success) {
|
||||
return { success: false, error: imapResult.error || 'IMAP-Fehler beim Wiederherstellen' };
|
||||
}
|
||||
|
||||
// In DB als wiederhergestellt markieren
|
||||
await prisma.cachedEmail.update({
|
||||
where: { id },
|
||||
data: {
|
||||
isDeleted: false,
|
||||
deletedAt: null,
|
||||
uid: imapResult.newUid || email.uid, // Neue UID im wiederhergestellten Ordner
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// E-Mail endgültig löschen (aus Papierkorb)
|
||||
export async function permanentDeleteEmail(id: number): Promise<TrashOperationResult> {
|
||||
// E-Mail laden
|
||||
const email = await prisma.cachedEmail.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
stressfreiEmail: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
return { success: false, error: 'E-Mail nicht gefunden' };
|
||||
}
|
||||
|
||||
if (!email.isDeleted) {
|
||||
return { success: false, error: 'E-Mail muss erst in den Papierkorb verschoben werden' };
|
||||
}
|
||||
|
||||
// IMAP-Einstellungen und Credentials
|
||||
const settings = await getImapSmtpSettings();
|
||||
if (!settings) {
|
||||
return { success: false, error: 'Keine E-Mail-Provider-Einstellungen' };
|
||||
}
|
||||
|
||||
if (!email.stressfreiEmail.emailPasswordEncrypted) {
|
||||
return { success: false, error: 'Keine Mailbox-Zugangsdaten' };
|
||||
}
|
||||
|
||||
let password: string;
|
||||
try {
|
||||
password = decrypt(email.stressfreiEmail.emailPasswordEncrypted);
|
||||
} catch {
|
||||
return { success: false, error: 'Passwort konnte nicht entschlüsselt werden' };
|
||||
}
|
||||
|
||||
const credentials: ImapCredentials = {
|
||||
host: settings.imapServer,
|
||||
port: settings.imapPort,
|
||||
user: email.stressfreiEmail.email,
|
||||
password,
|
||||
encryption: settings.imapEncryption,
|
||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||
};
|
||||
|
||||
// Auf IMAP-Server endgültig löschen (aus Trash)
|
||||
const imapResult = await permanentDelete(credentials, email.uid);
|
||||
|
||||
if (!imapResult.success) {
|
||||
return { success: false, error: imapResult.error || 'IMAP-Fehler beim endgültigen Löschen' };
|
||||
}
|
||||
|
||||
// Aus DB löschen
|
||||
await prisma.cachedEmail.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Papierkorb-E-Mails für einen Kunden abrufen
|
||||
export async function getTrashEmails(customerId: number): Promise<CachedEmailWithRelations[]> {
|
||||
return prisma.cachedEmail.findMany({
|
||||
where: {
|
||||
isDeleted: true,
|
||||
stressfreiEmail: {
|
||||
customerId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
stressfreiEmail: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
customerId: true,
|
||||
},
|
||||
},
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { deletedAt: 'desc' },
|
||||
}) as Promise<CachedEmailWithRelations[]>;
|
||||
}
|
||||
|
||||
// Papierkorb-E-Mails zählen
|
||||
export async function getTrashCount(customerId: number): Promise<number> {
|
||||
return prisma.cachedEmail.count({
|
||||
where: {
|
||||
isDeleted: true,
|
||||
stressfreiEmail: {
|
||||
customerId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy: E-Mail löschen (jetzt deprecated, nutze moveEmailToTrash)
|
||||
export async function deleteCachedEmail(id: number): Promise<void> {
|
||||
// Zur Abwärtskompatibilität: In Papierkorb verschieben statt löschen
|
||||
const result = await moveEmailToTrash(id);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Fehler beim Löschen');
|
||||
}
|
||||
}
|
||||
|
||||
// Alle gecachten E-Mails für eine StressfreiEmail löschen (nur Cache, kein IMAP)
|
||||
export async function clearCacheForAccount(stressfreiEmailId: number): Promise<number> {
|
||||
const result = await prisma.cachedEmail.deleteMany({
|
||||
where: { stressfreiEmailId },
|
||||
});
|
||||
return result.count;
|
||||
}
|
||||
|
||||
// ==================== SENT EMAILS ====================
|
||||
|
||||
// Gesendete E-Mail speichern
|
||||
export async function createSentEmail(
|
||||
stressfreiEmailId: number,
|
||||
params: SentEmailParams
|
||||
): Promise<CachedEmail> {
|
||||
// StressfreiEmail laden um Absender-Info zu bekommen
|
||||
const stressfreiEmail = await prisma.stressfreiEmail.findUnique({
|
||||
where: { id: stressfreiEmailId },
|
||||
select: { email: true },
|
||||
});
|
||||
|
||||
if (!stressfreiEmail) {
|
||||
throw new Error('StressfreiEmail nicht gefunden');
|
||||
}
|
||||
|
||||
const hasAttachments = params.attachmentNames && params.attachmentNames.length > 0;
|
||||
|
||||
const email = await prisma.cachedEmail.create({
|
||||
data: {
|
||||
stressfreiEmailId,
|
||||
folder: EmailFolder.SENT,
|
||||
messageId: params.messageId,
|
||||
uid: params.uid || 0, // UID vom IMAP Sent-Ordner (für Attachment-Download)
|
||||
subject: params.subject || null,
|
||||
fromAddress: stressfreiEmail.email,
|
||||
fromName: null,
|
||||
toAddresses: JSON.stringify(params.to),
|
||||
ccAddresses: params.cc && params.cc.length > 0 ? JSON.stringify(params.cc) : null,
|
||||
receivedAt: new Date(), // Sendezeitpunkt
|
||||
textBody: params.text || null,
|
||||
htmlBody: params.html || null,
|
||||
hasAttachments: hasAttachments || false,
|
||||
attachmentNames: hasAttachments ? JSON.stringify(params.attachmentNames) : null,
|
||||
isRead: true, // Gesendete sind immer "gelesen"
|
||||
isStarred: false,
|
||||
// Vertragszuordnung falls angegeben (automatisch = aus Vertrag gesendet)
|
||||
contractId: params.contractId || null,
|
||||
assignedAt: params.contractId ? new Date() : null,
|
||||
isAutoAssigned: params.contractId ? true : false, // Automatisch wenn aus Vertrag gesendet
|
||||
},
|
||||
});
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
// Anzahl E-Mails für Konto nach Ordner
|
||||
export async function getEmailCountByFolder(
|
||||
stressfreiEmailId: number,
|
||||
folder: 'INBOX' | 'SENT'
|
||||
): Promise<number> {
|
||||
return prisma.cachedEmail.count({
|
||||
where: {
|
||||
stressfreiEmailId,
|
||||
folder: folder === 'SENT' ? EmailFolder.SENT : EmailFolder.INBOX,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user