feat: lancer manuellement les imports SFR

This commit is contained in:
Manus
2026-09-30 14:45:00 +00:00
committed by manus-admin
parent 0d65592d75
commit 584e9631e5
2 changed files with 163 additions and 32 deletions

View File

@@ -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);
});
});

View File

@@ -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<void>;
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<boolean> {
export async function startConfiguredEmailImportService(
config: EmailImportConfig,
frequencyMinutes: number,
connectionTester: EmailConnectionTester = testImapConnection,
emailCheckRunner: EmailCheckRunner = runEmailCheckExclusive,
): Promise<EmailImportServiceStartResult> {
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<EmailImportServiceStartResult> {
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<boolean>
// 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<boolean>
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<EmailImportServiceStartResult>;
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,