@@ -4,7 +4,7 @@
import { CachedEmail , Prisma , EmailFolder } from '@prisma/client' ;
import prisma from '../lib/prisma.js' ;
import { decrypt } from '../utils/encryption.js' ;
import { fetchEmails , ImapCredentials , FetchedEmail , moveToTrash , restoreFromTrash , permanentDelete , humanizeImapError } from './imapService.js' ;
import { fetchEmails , ImapCredentials , FetchedEmail , moveToTrash , restoreFromTrash , permanentDelete , humanizeImapError , findJunkFolderPath } from './imapService.js' ;
import { getImapSmtpSettings } from './emailProvider/emailProviderService.js' ;
// ==================== TYPES ====================
@@ -68,7 +68,7 @@ export interface EmailListOptions {
// E-Mails für eine StressfreiEmail synchronisieren
export async function syncEmailsForAccount (
stressfreiEmailId : number ,
options ? : { folder? : string ; fullSync? : boolean }
options ? : { folder? : string ; fullSync? : boolean ; dbFolder? : EmailFolder }
) : Promise < SyncResult > {
const folder = options ? . folder || 'INBOX' ;
const fullSync = options ? . fullSync || false ;
@@ -111,10 +111,12 @@ export async function syncEmailsForAccount(
allowSelfSignedCerts : settings.allowSelfSignedCerts ,
} ;
// Folder-Mapping: IMAP-Ordner zu DB-Ordner
const dbFolder = folder . toUpperCase ( ) === 'SENT' || folder . toLowerCase ( ) === 'sent'
? EmailFolder.SENT
: EmailFolder.INBOX ;
// Folder-Mapping: IMAP-Ordner zu DB-Ordner. Explizites dbFolder (z.B.
// SPAM für den Junk-Ordner, dessen IMAP-Name variiert) hat Vorrang.
const dbFolder = options ? . 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 ;
@@ -168,7 +170,33 @@ export async function syncEmailsForAccount(
}
}
// Alle Ordner synchronisieren (INBOX + SENT)
/**
* Baut die IMAP-Credentials für ein Postfach (für Operationen, die eine
* eigene Verbindung brauchen – z.B. Junk-Ordner-Pfad ermitteln). Gibt null
* zurück, wenn kein Postfach/Passwort/Provider-Settings vorhanden sind.
*/
async function getImapCredentialsForAccount ( stressfreiEmailId : number ) : Promise < ImapCredentials | null > {
const stressfreiEmail = await prisma . stressfreiEmail . findUnique ( { where : { id : stressfreiEmailId } } ) ;
if ( ! stressfreiEmail ? . hasMailbox || ! stressfreiEmail . emailPasswordEncrypted ) return null ;
const settings = await getImapSmtpSettings ( ) ;
if ( ! settings ) return null ;
let password : string ;
try {
password = decrypt ( stressfreiEmail . emailPasswordEncrypted ) ;
} catch {
return null ;
}
return {
host : settings.imapServer ,
port : settings.imapPort ,
user : stressfreiEmail.email ,
password ,
encryption : settings.imapEncryption ,
allowSelfSignedCerts : settings.allowSelfSignedCerts ,
} ;
}
// Alle Ordner synchronisieren (INBOX + SENT + SPAM/Junk)
export async function syncAllFoldersForAccount (
stressfreiEmailId : number ,
options ? : { fullSync? : boolean }
@@ -181,6 +209,23 @@ export async function syncAllFoldersForAccount(
// SENT synchronisieren (Plesk verwendet "Sent" als Ordnername)
const sentResult = await syncEmailsForAccount ( stressfreiEmailId , { folder : 'Sent' , fullSync } ) ;
// SPAM/Junk synchronisieren – Ordnername variiert je Server, daher erst
// per IMAP ermitteln. Existiert kein Junk-Ordner, wird Spam übersprungen
// (kein Fehler). Wird explizit als dbFolder=SPAM abgelegt.
let spamNew = 0 ;
const credentials = await getImapCredentialsForAccount ( stressfreiEmailId ) ;
if ( credentials ) {
const junkPath = await findJunkFolderPath ( credentials ) ;
if ( junkPath ) {
const spamResult = await syncEmailsForAccount ( stressfreiEmailId , {
folder : junkPath ,
dbFolder : EmailFolder.SPAM ,
fullSync ,
} ) ;
if ( spamResult . success ) spamNew = spamResult . newEmails ;
}
}
// Ergebnisse kombinieren
if ( ! inboxResult . success && ! sentResult . success ) {
return {
@@ -193,7 +238,7 @@ export async function syncAllFoldersForAccount(
return {
success : true ,
newEmails : inboxResult.newEmails + sentResult . newEmails ,
newEmails : inboxResult.newEmails + sentResult . newEmails + spamNew ,
totalEmails : inboxResult.totalEmails ,
} ;
}
@@ -280,9 +325,11 @@ export async function getCachedEmails(
where . contractId = options . contractId ;
}
// Folder-Filter (INBOX oder SENT )
// Folder-Filter (INBOX, SENT oder SPAM )
if ( options . folder === 'SENT' ) {
where . folder = EmailFolder . SENT ;
} else if ( options . folder === 'SPAM' ) {
where . folder = EmailFolder . SPAM ;
} else if ( options . folder === 'INBOX' || ! options . folder ) {
// Standard: Nur Posteingang
where . folder = EmailFolder . INBOX ;
@@ -573,10 +620,12 @@ export async function getFolderCountsForAccount(stressfreiEmailId: number): Prom
inboxUnread : number ;
sent : number ;
sentUnread : number ;
spam : number ;
spamUnread : number ;
trash : number ;
trashUnread : number ;
} > {
const [ inbox , inboxUnread , sent , sentUnread , trash , trashUnread ] = await Promise . all ( [
const [ inbox , inboxUnread , sent , sentUnread , spam , spamUnread , trash , trashUnread ] = await Promise . all ( [
// INBOX total
prisma . cachedEmail . count ( {
where : { stressfreiEmailId , folder : EmailFolder.INBOX , isDeleted : false } ,
@@ -593,6 +642,14 @@ export async function getFolderCountsForAccount(stressfreiEmailId: number): Prom
prisma . cachedEmail . count ( {
where : { stressfreiEmailId , folder : EmailFolder.SENT , isDeleted : false , isRead : false } ,
} ) ,
// SPAM total
prisma . cachedEmail . count ( {
where : { stressfreiEmailId , folder : EmailFolder.SPAM , isDeleted : false } ,
} ) ,
// SPAM unread
prisma . cachedEmail . count ( {
where : { stressfreiEmailId , folder : EmailFolder.SPAM , isDeleted : false , isRead : false } ,
} ) ,
// TRASH total (isDeleted = true)
prisma . cachedEmail . count ( {
where : { stressfreiEmailId , isDeleted : true } ,
@@ -603,7 +660,7 @@ export async function getFolderCountsForAccount(stressfreiEmailId: number): Prom
} ) ,
] ) ;
return { inbox , inboxUnread , sent , sentUnread , trash , trashUnread } ;
return { inbox , inboxUnread , sent , sentUnread , spam , spamUnread , trash , trashUnread } ;
}
// E-Mail-Anzahl pro Ordner für einen Vertrag (zugeordnete E-Mails).
@@ -759,8 +816,17 @@ export async function moveEmailToTrash(id: number): Promise<TrashOperationResult
allowSelfSignedCerts : settings.allowSelfSignedCerts ,
} ;
// O rdner bestimmen (INBOX oder Sent)
const sourceFolder = email . folder === 'SENT' ? 'Sent' : 'INBOX' ;
// IMAP-Quello rdner bestimmen. Bei SPAM den tatsächlichen Junk-Pfad
// ermitteln (Name variiert je Server) – sonst würde moveToTrash im
// falschen Ordner nach der UID suchen.
let sourceFolder : string ;
if ( email . folder === 'SENT' ) {
sourceFolder = 'Sent' ;
} else if ( email . folder === 'SPAM' ) {
sourceFolder = ( await findJunkFolderPath ( credentials ) ) || 'Junk' ;
} else {
sourceFolder = 'INBOX' ;
}
// Auf IMAP-Server in Trash verschieben
const imapResult = await moveToTrash ( credentials , email . uid , sourceFolder ) ;
@@ -826,8 +892,16 @@ export async function restoreEmailFromTrash(id: number): Promise<TrashOperationR
allowSelfSignedCerts : settings.allowSelfSignedCerts ,
} ;
// Ziel-Ordner bestimmen (basierend auf originalem Ordner)
const targetFolder = email . folder === 'SENT' ? 'Sent' : 'INBOX' ;
// Ziel-Ordner bestimmen (basierend auf originalem Ordner). SPAM zurück
// in den tatsächlichen Junk-Ordner (Name variiert je Server).
let targetFolder : string ;
if ( email . folder === 'SENT' ) {
targetFolder = 'Sent' ;
} else if ( email . folder === 'SPAM' ) {
targetFolder = ( await findJunkFolderPath ( credentials ) ) || 'Junk' ;
} else {
targetFolder = 'INBOX' ;
}
// Auf IMAP-Server aus Trash wiederherstellen
const imapResult = await restoreFromTrash ( credentials , email . uid , targetFolder ) ;