Checkpoint: OAuth2 IMAP pour Office 365 : ajout du mode d'authentification OAuth2 pour l'import email (Azure AD Client Credentials), sélecteur basic/oauth2 dans l'UI, bouton "Tester la connexion IMAP", procédure testEmailConnection dans le router. Migration DB emailImportAuthMode appliquée.
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor";
|
||||
import { localStoragePut, generateStorageKey } from "./localStorage";
|
||||
import { sendImportNotification } from "./notificationService";
|
||||
import { getOffice365ImapToken, buildXOAuth2String } from "./office365OAuth";
|
||||
|
||||
interface EmailImportConfig {
|
||||
userId: number;
|
||||
@@ -20,6 +21,11 @@ interface EmailImportConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
sinceDate?: number; // Unix timestamp (s) — ne pas lire les emails antérieurs à cette date
|
||||
authMode?: "basic" | "oauth2"; // Mode d'authentification IMAP
|
||||
// Credentials Azure AD pour OAuth2
|
||||
azureTenantId?: string;
|
||||
azureClientId?: string;
|
||||
azureClientSecret?: string;
|
||||
}
|
||||
|
||||
// Store active intervals for each user
|
||||
@@ -216,25 +222,66 @@ async function processEmailAttachment(
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to IMAP and process unread emails with PDF attachments
|
||||
* Construit la configuration IMAP en fonction du mode d'authentification.
|
||||
* - basic : login/password classique
|
||||
* - oauth2 : obtient un token Azure AD et utilise XOAUTH2
|
||||
*/
|
||||
async function checkEmailsForPDFs(config: EmailImportConfig): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const imap = new Imap({
|
||||
async function buildImapConfig(config: EmailImportConfig): Promise<Imap.Config> {
|
||||
if (config.authMode === "oauth2") {
|
||||
if (!config.azureTenantId || !config.azureClientId || !config.azureClientSecret) {
|
||||
throw new Error(
|
||||
"OAuth2 IMAP : credentials Azure AD incomplets (Tenant ID, Client ID, Client Secret requis)"
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[EmailImport] Obtaining OAuth2 token for ${config.emailAddress}...`);
|
||||
const accessToken = await getOffice365ImapToken(
|
||||
config.azureTenantId,
|
||||
config.azureClientId,
|
||||
config.azureClientSecret
|
||||
);
|
||||
const xoauth2 = buildXOAuth2String(config.emailAddress, accessToken);
|
||||
console.log(`[EmailImport] OAuth2 token obtained successfully`);
|
||||
|
||||
return {
|
||||
user: config.emailAddress,
|
||||
password: config.password,
|
||||
xoauth2,
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
tls: true,
|
||||
tlsOptions: { rejectUnauthorized: false },
|
||||
});
|
||||
authTimeout: 30000,
|
||||
} as any;
|
||||
}
|
||||
|
||||
// Basic auth (par défaut)
|
||||
return {
|
||||
user: config.emailAddress,
|
||||
password: config.password,
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
tls: true,
|
||||
tlsOptions: { rejectUnauthorized: false },
|
||||
authTimeout: 30000,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to IMAP and process unread emails with PDF attachments
|
||||
*/
|
||||
async function checkEmailsForPDFs(config: EmailImportConfig): Promise<void> {
|
||||
// Build IMAP config (may involve async OAuth2 token fetch)
|
||||
const imapConfig = await buildImapConfig(config);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const imap = new Imap(imapConfig);
|
||||
|
||||
function openInbox(cb: (err: Error | null, box?: any) => void) {
|
||||
imap.openBox("INBOX", false, cb);
|
||||
}
|
||||
|
||||
imap.once("ready", () => {
|
||||
console.log(`[EmailImport] Connected to IMAP server for user ${config.userId}`);
|
||||
console.log(`[EmailImport] Connected to IMAP server for user ${config.userId} (mode: ${config.authMode || "basic"})`);
|
||||
|
||||
openInbox((err, box) => {
|
||||
if (err) {
|
||||
@@ -294,7 +341,6 @@ async function checkEmailsForPDFs(config: EmailImportConfig): Promise<void> {
|
||||
);
|
||||
|
||||
if (pdfAttachments.length === 0) {
|
||||
console.log(`[EmailImport] Email ${seqno} has no PDF attachments, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -380,6 +426,65 @@ async function checkEmailsForPDFs(config: EmailImportConfig): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test IMAP connection for a user (without processing emails)
|
||||
* Returns detailed error message if connection fails
|
||||
*/
|
||||
export async function testImapConnection(config: EmailImportConfig): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const imapConfig = await buildImapConfig(config);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const imap = new Imap(imapConfig);
|
||||
let resolved = false;
|
||||
|
||||
const done = (result: { success: boolean; message: string }) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
try { imap.destroy(); } catch {}
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
|
||||
imap.once("ready", () => {
|
||||
console.log(`[EmailImport] Test connection successful for ${config.emailAddress}`);
|
||||
done({ success: true, message: `Connexion IMAP réussie pour ${config.emailAddress}` });
|
||||
});
|
||||
|
||||
imap.once("error", (err: any) => {
|
||||
console.error(`[EmailImport] Test connection failed:`, err);
|
||||
let message = `Erreur de connexion IMAP : ${err.message || err}`;
|
||||
|
||||
// Messages d'erreur plus clairs
|
||||
if (err.message?.includes("Invalid credentials") || err.message?.includes("AUTHENTICATE")) {
|
||||
if (config.authMode === "oauth2") {
|
||||
message = "Authentification OAuth2 refusée. Vérifiez que l'application Azure AD a bien la permission IMAP.AccessAsApp et que le consentement admin a été accordé.";
|
||||
} else {
|
||||
message = "Identifiants invalides. Pour Office 365, l'authentification basique est désactivée. Activez le mode OAuth2 et configurez les credentials Azure AD.";
|
||||
}
|
||||
} else if (err.message?.includes("ECONNREFUSED") || err.message?.includes("ENOTFOUND")) {
|
||||
message = `Impossible de se connecter au serveur ${config.host}:${config.port}. Vérifiez l'adresse et le port IMAP.`;
|
||||
} else if (err.message?.includes("certificate") || err.message?.includes("SSL")) {
|
||||
message = `Erreur SSL/TLS lors de la connexion à ${config.host}. Vérifiez le port (993 pour SSL).`;
|
||||
} else if (err.message?.includes("timeout") || err.message?.includes("Timeout")) {
|
||||
message = `Timeout de connexion à ${config.host}:${config.port}. Vérifiez l'adresse du serveur IMAP.`;
|
||||
}
|
||||
|
||||
done({ success: false, message });
|
||||
});
|
||||
|
||||
// Timeout de sécurité
|
||||
setTimeout(() => {
|
||||
done({ success: false, message: `Timeout : impossible de se connecter à ${config.host}:${config.port} dans les 15 secondes.` });
|
||||
}, 15000);
|
||||
|
||||
imap.connect();
|
||||
});
|
||||
} catch (error: any) {
|
||||
return { success: false, message: `Erreur : ${error.message || error}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start email import service for a user
|
||||
*/
|
||||
@@ -393,27 +498,43 @@ export async function startEmailImportService(userId: number): Promise<boolean>
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!settings.emailImportAddress || !settings.emailImportPassword || !settings.emailImportHost) {
|
||||
if (!settings.emailImportAddress || !settings.emailImportHost) {
|
||||
console.log(`[EmailImport] Email import configuration incomplete for user ${userId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const authMode = (settings as any).emailImportAuthMode as "basic" | "oauth2" || "basic";
|
||||
|
||||
// Vérification selon le mode d'auth
|
||||
if (authMode === "basic" && !settings.emailImportPassword) {
|
||||
console.log(`[EmailImport] Email import password missing for user ${userId}`);
|
||||
return false;
|
||||
}
|
||||
if (authMode === "oauth2" && (!settings.azureTenantId || !settings.azureClientId || !settings.azureClientSecret)) {
|
||||
console.log(`[EmailImport] OAuth2 credentials incomplete for user ${userId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stop existing service if running
|
||||
stopEmailImportService(userId);
|
||||
|
||||
const config: EmailImportConfig = {
|
||||
userId,
|
||||
emailAddress: settings.emailImportAddress,
|
||||
password: settings.emailImportPassword,
|
||||
password: settings.emailImportPassword || "",
|
||||
host: settings.emailImportHost,
|
||||
port: settings.emailImportPort || 993,
|
||||
sinceDate: settings.emailImportSinceDate ?? undefined,
|
||||
authMode,
|
||||
azureTenantId: settings.azureTenantId || undefined,
|
||||
azureClientId: settings.azureClientId || undefined,
|
||||
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`
|
||||
`[EmailImport] Starting email import service for user ${userId} with frequency ${settings.emailImportFrequency} minutes (auth: ${authMode})`
|
||||
);
|
||||
|
||||
// Run immediately on start
|
||||
@@ -466,26 +587,32 @@ export async function triggerEmailCheck(userId: number): Promise<{ success: bool
|
||||
const settings = await getImportSettingsByUser(userId);
|
||||
|
||||
if (!settings || settings.emailImportEnabled !== 1) {
|
||||
return { success: false, message: "Import par email non activ\u00e9" };
|
||||
return { success: false, message: "Import par email non activé" };
|
||||
}
|
||||
|
||||
if (!settings.emailImportAddress || !settings.emailImportPassword || !settings.emailImportHost) {
|
||||
return { success: false, message: "Configuration IMAP incompl\u00e8te" };
|
||||
if (!settings.emailImportAddress || !settings.emailImportHost) {
|
||||
return { success: false, message: "Configuration IMAP incomplète" };
|
||||
}
|
||||
|
||||
const authMode = (settings as any).emailImportAuthMode as "basic" | "oauth2" || "basic";
|
||||
|
||||
const config: EmailImportConfig = {
|
||||
userId,
|
||||
emailAddress: settings.emailImportAddress,
|
||||
password: settings.emailImportPassword,
|
||||
password: settings.emailImportPassword || "",
|
||||
host: settings.emailImportHost,
|
||||
port: settings.emailImportPort || 993,
|
||||
sinceDate: settings.emailImportSinceDate ?? undefined,
|
||||
authMode,
|
||||
azureTenantId: settings.azureTenantId || undefined,
|
||||
azureClientId: settings.azureClientId || undefined,
|
||||
azureClientSecret: settings.azureClientSecret || undefined,
|
||||
};
|
||||
|
||||
console.log(`[EmailImport] Manual check triggered for user ${userId}`);
|
||||
await checkEmailsForPDFs(config);
|
||||
|
||||
return { success: true, message: "V\u00e9rification termin\u00e9e avec succ\u00e8s" };
|
||||
return { success: true, message: "Vérification terminée avec succès" };
|
||||
} catch (error: any) {
|
||||
console.error(`[EmailImport] Error during manual check for user ${userId}:`, error);
|
||||
return { success: false, message: `Erreur: ${error.message}` };
|
||||
|
||||
Reference in New Issue
Block a user