Checkpoint: Historique des imports enrichi avec le compte, la source, le déclencheur, les résultats, les erreurs et les informations du fichier source. Les administrateurs peuvent déclencher une lecture e-mail ponctuelle pour une boîte configurée, avec confirmation explicite, sans activer ni créer de planification. Migration importTrigger appliquée en sandbox ; TypeScript, 48 tests, build et rendus bureau/mobile validés.
This commit is contained in:
@@ -32,6 +32,40 @@ export interface EmailImportConfig {
|
||||
azureClientSecret?: string;
|
||||
}
|
||||
|
||||
export type EmailImportTrigger = "manual" | "automatic";
|
||||
|
||||
type EmailImportSettingsSnapshot = {
|
||||
emailImportEnabled: number;
|
||||
emailImportAddress: string | null;
|
||||
emailImportPassword: string | null;
|
||||
emailImportHost: string | null;
|
||||
emailImportPort: number | null;
|
||||
emailImportSinceDate: number | null;
|
||||
emailImportAuthMode: "basic" | "oauth2";
|
||||
azureTenantId: string | null;
|
||||
azureClientId: string | null;
|
||||
azureClientSecret: string | null;
|
||||
};
|
||||
|
||||
/** Vérifie une configuration IMAP sans tenir compte de l'activation de la planification. */
|
||||
export function validateEmailImportConfiguration(
|
||||
settings: EmailImportSettingsSnapshot | null | undefined,
|
||||
): string | null {
|
||||
if (!settings || !settings.emailImportAddress || !settings.emailImportHost) {
|
||||
return "Configuration IMAP incomplète";
|
||||
}
|
||||
if (settings.emailImportAuthMode === "basic" && !settings.emailImportPassword) {
|
||||
return "Mot de passe IMAP manquant";
|
||||
}
|
||||
if (
|
||||
settings.emailImportAuthMode === "oauth2" &&
|
||||
(!settings.azureTenantId || !settings.azureClientId || !settings.azureClientSecret)
|
||||
) {
|
||||
return "Configuration OAuth2 IMAP incomplète";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit les options ImapFlow sans effectuer d'appel réseau.
|
||||
* ImapFlow reçoit le jeton brut et construit lui-même SASL XOAUTH2.
|
||||
@@ -67,14 +101,17 @@ const activeIntervals = new Map<number, NodeJS.Timeout>();
|
||||
// qu'un second cycle IMAP traite les mêmes messages avant la fin du premier.
|
||||
const activeChecks = new Map<number, Promise<void>>();
|
||||
|
||||
function runEmailCheckExclusive(config: EmailImportConfig): Promise<void> {
|
||||
function runEmailCheckExclusive(
|
||||
config: EmailImportConfig,
|
||||
trigger: EmailImportTrigger = "automatic",
|
||||
): Promise<void> {
|
||||
const runningCheck = activeChecks.get(config.userId);
|
||||
if (runningCheck) {
|
||||
console.log(`[EmailImport] Vérification déjà en cours pour user ${config.userId}, cycle ignoré`);
|
||||
return runningCheck;
|
||||
}
|
||||
|
||||
const check = checkEmailsForPDFs(config).finally(() => {
|
||||
const check = checkEmailsForPDFs(config, trigger).finally(() => {
|
||||
if (activeChecks.get(config.userId) === check) activeChecks.delete(config.userId);
|
||||
});
|
||||
activeChecks.set(config.userId, check);
|
||||
@@ -88,7 +125,8 @@ function runEmailCheckExclusive(config: EmailImportConfig): Promise<void> {
|
||||
async function processEmailAttachment(
|
||||
userId: number,
|
||||
attachment: Attachment,
|
||||
emailSubject: string
|
||||
emailSubject: string,
|
||||
trigger: EmailImportTrigger,
|
||||
): Promise<{ success: boolean; totalInvoices: number; imported: number; duplicates: number; errors: number; quotaError?: boolean }> {
|
||||
const fileName = attachment.filename || `email-attachment-${Date.now()}.pdf`;
|
||||
console.log(`[EmailImport] Processing attachment: ${fileName} from email: ${emailSubject}`);
|
||||
@@ -323,6 +361,7 @@ async function processEmailAttachment(
|
||||
errorDetails: errorDetails.length > 0 ? JSON.stringify(errorDetails) : null,
|
||||
warningMessage,
|
||||
importSource: "email",
|
||||
importTrigger: trigger,
|
||||
});
|
||||
|
||||
console.log(`[EmailImport] Successfully processed attachment: ${fileName}`);
|
||||
@@ -386,7 +425,10 @@ async function buildImapConfig(config: EmailImportConfig): Promise<ImapFlowOptio
|
||||
/**
|
||||
* Connect to IMAP and process unread emails with PDF attachments
|
||||
*/
|
||||
async function checkEmailsForPDFs(config: EmailImportConfig): Promise<void> {
|
||||
async function checkEmailsForPDFs(
|
||||
config: EmailImportConfig,
|
||||
trigger: EmailImportTrigger,
|
||||
): Promise<void> {
|
||||
const imapConfig = await buildImapConfig(config);
|
||||
const client = new ImapFlow(imapConfig);
|
||||
client.on("error", (error) => {
|
||||
@@ -450,6 +492,7 @@ async function checkEmailsForPDFs(config: EmailImportConfig): Promise<void> {
|
||||
config.userId,
|
||||
attachment,
|
||||
parsed.subject || "No subject",
|
||||
trigger,
|
||||
);
|
||||
allAttachmentsSucceeded = allAttachmentsSucceeded && result.success;
|
||||
|
||||
@@ -615,29 +658,28 @@ export function isEmailImportServiceRunning(userId: number): boolean {
|
||||
return activeIntervals.has(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger an immediate email check for a user
|
||||
*/
|
||||
export async function triggerEmailCheck(userId: number): Promise<{ success: boolean; message: string }> {
|
||||
async function triggerConfiguredEmailCheck(
|
||||
userId: number,
|
||||
requireAutomaticEnabled: boolean,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
// Get user's import settings
|
||||
const settings = await getImportSettingsByUser(userId);
|
||||
|
||||
if (!settings || settings.emailImportEnabled !== 1) {
|
||||
|
||||
if (!settings || (requireAutomaticEnabled && settings.emailImportEnabled !== 1)) {
|
||||
return { success: false, message: "Import par email non activé" };
|
||||
}
|
||||
|
||||
if (!settings.emailImportAddress || !settings.emailImportHost) {
|
||||
return { success: false, message: "Configuration IMAP incomplète" };
|
||||
}
|
||||
const validationError = validateEmailImportConfiguration(settings);
|
||||
if (validationError) return { success: false, message: validationError };
|
||||
|
||||
const authMode = (settings as any).emailImportAuthMode as "basic" | "oauth2" || "basic";
|
||||
|
||||
const config: EmailImportConfig = {
|
||||
userId,
|
||||
emailAddress: settings.emailImportAddress,
|
||||
// validateEmailImportConfiguration garantit ces deux valeurs avant ce point.
|
||||
emailAddress: settings.emailImportAddress!,
|
||||
password: settings.emailImportPassword || "",
|
||||
host: settings.emailImportHost,
|
||||
host: settings.emailImportHost!,
|
||||
port: settings.emailImportPort || 993,
|
||||
sinceDate: settings.emailImportSinceDate ?? undefined,
|
||||
authMode,
|
||||
@@ -647,7 +689,8 @@ export async function triggerEmailCheck(userId: number): Promise<{ success: bool
|
||||
};
|
||||
|
||||
console.log(`[EmailImport] Manual check triggered for user ${userId}`);
|
||||
await runEmailCheckExclusive(config);
|
||||
// Cette voie exécute une vérification unique : elle ne crée pas de setInterval.
|
||||
await runEmailCheckExclusive(config, "manual");
|
||||
|
||||
return { success: true, message: "Vérification terminée avec succès" };
|
||||
} catch (error: any) {
|
||||
@@ -656,6 +699,16 @@ export async function triggerEmailCheck(userId: number): Promise<{ success: bool
|
||||
}
|
||||
}
|
||||
|
||||
/** Déclenchement manuel réservé aux appels administrateurs, même si le planificateur est désactivé. */
|
||||
export async function triggerManualEmailCheck(userId: number): Promise<{ success: boolean; message: string }> {
|
||||
return triggerConfiguredEmailCheck(userId, false);
|
||||
}
|
||||
|
||||
/** Compatibilité avec le bouton existant : la vérification personnelle exige toujours l’activation automatique. */
|
||||
export async function triggerEmailCheck(userId: number): Promise<{ success: boolean; message: string }> {
|
||||
return triggerConfiguredEmailCheck(userId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all email import services
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user