diff --git a/server/emailImportService.test.ts b/server/emailImportService.test.ts index 2d151b7..6a4dc90 100644 --- a/server/emailImportService.test.ts +++ b/server/emailImportService.test.ts @@ -1,7 +1,11 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { + createEmailImportAccountController, createImapFlowOptions, type EmailImportConfig, + isEmailImportServiceRunning, + startConfiguredEmailImportService, + stopEmailImportService, validateEmailImportConfiguration, } from "./emailImportService"; @@ -76,3 +80,71 @@ describe("validateEmailImportConfiguration", () => { expect(error).toBe("Configuration IMAP incomplète"); }); }); + +describe("startConfiguredEmailImportService", () => { + it("ne crée aucune planification si le test IMAP préalable échoue", async () => { + const userId = 901; + stopEmailImportService(userId); + + const result = await startConfiguredEmailImportService( + { ...baseConfig, userId, authMode: "basic" }, + 5, + async () => ({ + success: false, + message: "Authentification IMAP refusée.", + }), + ); + + expect(result).toEqual({ + success: false, + message: "Authentification IMAP refusée.", + }); + expect(isEmailImportServiceRunning(userId)).toBe(false); + }); + + it("démarre après un test IMAP concluant et exécute le contrôle initial", async () => { + const userId = 902; + const runEmailCheck = vi.fn(async () => undefined); + + try { + const result = await startConfiguredEmailImportService( + { ...baseConfig, userId, authMode: "basic" }, + 5, + async () => ({ success: true, message: "Connexion IMAP réussie." }), + runEmailCheck, + ); + + expect(result).toEqual({ + success: true, + message: "Service de réception e-mail démarré", + }); + expect(isEmailImportServiceRunning(userId)).toBe(true); + expect(runEmailCheck).toHaveBeenCalledWith(expect.objectContaining({ userId })); + } finally { + stopEmailImportService(userId); + } + }); +}); + +describe("createEmailImportAccountController", () => { + it("cible exclusivement le compte sélectionné par un administrateur", async () => { + const start = vi.fn(async () => ({ success: true, message: "Service de réception e-mail démarré" })); + const stop = vi.fn(); + const isRunning = vi.fn(() => true); + const controller = createEmailImportAccountController({ start, stop, isRunning }); + + await expect(controller.startAccount(42)).resolves.toEqual({ + success: true, + message: "Service de réception e-mail démarré", + }); + expect(start).toHaveBeenCalledWith(42); + + expect(controller.stopAccount(42)).toEqual({ + success: true, + message: "Service de réception e-mail arrêté", + }); + expect(stop).toHaveBeenCalledWith(42); + expect(controller.accountStatus(42)).toEqual({ isRunning: true }); + expect(isRunning).toHaveBeenCalledWith(42); + }); +}); diff --git a/server/emailImportService.ts b/server/emailImportService.ts index c6f51da..f218951 100644 --- a/server/emailImportService.ts +++ b/server/emailImportService.ts @@ -34,6 +34,17 @@ export interface EmailImportConfig { export type EmailImportTrigger = "manual" | "automatic"; +export type EmailImportServiceStartResult = { + success: boolean; + message: string; +}; + +type EmailConnectionTester = ( + config: EmailImportConfig, +) => Promise<{ success: boolean; message: string }>; + +type EmailCheckRunner = (config: EmailImportConfig) => Promise; + type EmailImportSettingsSnapshot = { emailImportEnabled: number; emailImportAddress: string | null; @@ -566,21 +577,63 @@ export async function testImapConnection(config: EmailImportConfig): Promise<{ s } /** - * Start email import service for a user + * Vérifie la boîte avant de créer la planification. Une authentification refusée + * ne doit jamais produire une boucle de tentatives automatiques sans fin. */ -export async function startEmailImportService(userId: number): Promise { +export async function startConfiguredEmailImportService( + config: EmailImportConfig, + frequencyMinutes: number, + connectionTester: EmailConnectionTester = testImapConnection, + emailCheckRunner: EmailCheckRunner = runEmailCheckExclusive, +): Promise { + const connection = await connectionTester(config); + if (!connection.success) { + console.error( + `[EmailImport] Service non démarré pour user ${config.userId}: ${connection.message}`, + ); + return connection; + } + + // Stop existing service if running only after the new configuration has passed validation. + stopEmailImportService(config.userId); + + const frequencyMs = Math.max(1, frequencyMinutes || 30) * 60 * 1000; + console.log( + `[EmailImport] Starting email import service for user ${config.userId} with frequency ${frequencyMinutes || 30} minutes (auth: ${config.authMode || "basic"})`, + ); + + // Run immediately on start. The preflight above confirms that the account accepts the credentials. + emailCheckRunner(config).catch((error) => { + console.error(`[EmailImport] Error checking emails for user ${config.userId}:`, error); + }); + + const interval = setInterval(() => { + emailCheckRunner(config).catch((error) => { + console.error(`[EmailImport] Error checking emails for user ${config.userId}:`, error); + }); + }, frequencyMs); + + activeIntervals.set(config.userId, interval); + console.log(`[EmailImport] Email import service started for user ${config.userId}`); + return { success: true, message: "Service de réception e-mail démarré" }; +} + +/** + * Start email import service for a user. + */ +export async function startEmailImportService(userId: number): Promise { try { // Get user's import settings const settings = await getImportSettingsByUser(userId); if (!settings || settings.emailImportEnabled !== 1) { console.log(`[EmailImport] Email import not enabled for user ${userId}`); - return false; + return { success: false, message: "Activez et enregistrez la réception automatique avant de démarrer le service." }; } if (!settings.emailImportAddress || !settings.emailImportHost) { console.log(`[EmailImport] Email import configuration incomplete for user ${userId}`); - return false; + return { success: false, message: "Configuration IMAP incomplète : adresse e-mail ou serveur IMAP manquant." }; } const authMode = (settings as any).emailImportAuthMode as "basic" | "oauth2" || "basic"; @@ -588,16 +641,13 @@ export async function startEmailImportService(userId: number): Promise // Vérification selon le mode d'auth if (authMode === "basic" && !settings.emailImportPassword) { console.log(`[EmailImport] Email import password missing for user ${userId}`); - return false; + return { success: false, message: "Mot de passe IMAP manquant." }; } if (authMode === "oauth2" && (!settings.azureTenantId || !settings.azureClientId || !settings.azureClientSecret)) { console.log(`[EmailImport] OAuth2 credentials incomplete for user ${userId}`); - return false; + return { success: false, message: "Configuration OAuth2 IMAP incomplète : renseignez les paramètres Microsoft Azure AD." }; } - // Stop existing service if running - stopEmailImportService(userId); - const config: EmailImportConfig = { userId, emailAddress: settings.emailImportAddress, @@ -611,31 +661,13 @@ export async function startEmailImportService(userId: number): Promise azureClientSecret: settings.azureClientSecret || undefined, }; - const frequencyMs = (settings.emailImportFrequency || 30) * 60 * 1000; // Convert minutes to milliseconds - - console.log( - `[EmailImport] Starting email import service for user ${userId} with frequency ${settings.emailImportFrequency} minutes (auth: ${authMode})` + return startConfiguredEmailImportService( + config, + settings.emailImportFrequency || 30, ); - - // Run immediately on start - runEmailCheckExclusive(config).catch((error) => { - console.error(`[EmailImport] Error checking emails for user ${userId}:`, error); - }); - - // Set up interval for periodic checks - const interval = setInterval(() => { - runEmailCheckExclusive(config).catch((error) => { - console.error(`[EmailImport] Error checking emails for user ${userId}:`, error); - }); - }, frequencyMs); - - activeIntervals.set(userId, interval); - console.log(`[EmailImport] Email import service started for user ${userId}`); - - return true; } catch (error) { console.error(`[EmailImport] Error starting email import service for user ${userId}:`, error); - return false; + return { success: false, message: "Erreur technique lors du démarrage de la réception e-mail." }; } } @@ -658,6 +690,33 @@ export function isEmailImportServiceRunning(userId: number): boolean { return activeIntervals.has(userId); } +type EmailImportAccountControllerDependencies = { + start: (userId: number) => Promise; + stop: (userId: number) => void; + isRunning: (userId: number) => boolean; +}; + +/** + * Contrôles administratifs d'une boîte source. Les paramètres restent rattachés + * à leur compte propriétaire ; seul le service planifié est administré ici. + */ +export function createEmailImportAccountController( + dependencies: EmailImportAccountControllerDependencies = { + start: startEmailImportService, + stop: stopEmailImportService, + isRunning: isEmailImportServiceRunning, + }, +) { + return { + startAccount: (userId: number) => dependencies.start(userId), + stopAccount: (userId: number): EmailImportServiceStartResult => { + dependencies.stop(userId); + return { success: true, message: "Service de réception e-mail arrêté" }; + }, + accountStatus: (userId: number) => ({ isRunning: dependencies.isRunning(userId) }), + }; +} + async function triggerConfiguredEmailCheck( userId: number, requireAutomaticEnabled: boolean,