diff --git a/client/src/pages/ImportSettings.tsx b/client/src/pages/ImportSettings.tsx index 8abc776..6f93eeb 100644 --- a/client/src/pages/ImportSettings.tsx +++ b/client/src/pages/ImportSettings.tsx @@ -35,6 +35,7 @@ export default function ImportSettings() { const stopFolderServiceMutation = trpc.folderImportService.stop.useMutation(); const testAzureConnectionMutation = trpc.importSettings.testAzureConnection.useMutation(); const testSharePointUploadMutation = trpc.importSettings.testSharePointUpload.useMutation(); + const testEmailConnectionMutation = trpc.importSettings.testEmailConnection.useMutation(); // Manual import const [manualImportEnabled, setManualImportEnabled] = useState(true); @@ -48,6 +49,7 @@ export default function ImportSettings() { const [emailImportEnabled, setEmailImportEnabled] = useState(false); const [emailImportAddress, setEmailImportAddress] = useState(""); const [emailImportPassword, setEmailImportPassword] = useState(""); + const [emailImportAuthMode, setEmailImportAuthMode] = useState<"basic" | "oauth2">("basic"); const [emailImportHost, setEmailImportHost] = useState(""); const [emailImportPort, setEmailImportPort] = useState(993); const [emailImportFrequency, setEmailImportFrequency] = useState(30); @@ -76,6 +78,7 @@ export default function ImportSettings() { setEmailImportEnabled(settings.emailImportEnabled === 1); setEmailImportAddress(settings.emailImportAddress || ""); setEmailImportPassword(settings.emailImportPassword || ""); + setEmailImportAuthMode(((settings as any).emailImportAuthMode as "basic" | "oauth2") || "basic"); setEmailImportHost(settings.emailImportHost || ""); setEmailImportPort(settings.emailImportPort || 993); setEmailImportFrequency(settings.emailImportFrequency || 30); @@ -148,6 +151,7 @@ export default function ImportSettings() { emailImportSinceDate: emailImportSinceDate ? Math.floor(new Date(emailImportSinceDate + 'T00:00:00Z').getTime() / 1000) : null, + emailImportAuthMode: emailImportAuthMode, exportFolder: exportFolder || null, exportFolderType: exportFolderType, bapExportMode: computedExportMode() as "browser" | "folder" | "both", @@ -396,6 +400,40 @@ export default function ImportSettings() { {emailImportEnabled && (
+ {/* Mode d'authentification */} +
+ +
+ + +
+ {emailImportAuthMode === "oauth2" && ( +
+ Office 365 : Microsoft a désactivé l'auth basique pour Exchange Online. Utilisez ce mode avec les credentials Azure AD déjà configurés dans l'onglet Export. +
+ )} +
+
+ {emailImportAuthMode === "basic" && (
+ )} + {emailImportAuthMode === "oauth2" && ( +
+ + En mode OAuth2, les credentials Azure AD (Tenant ID, Client ID, Client Secret) configurés dans l'onglet Export > SharePoint seront utilisés automatiquement. +
+ )}
@@ -475,6 +521,34 @@ export default function ImportSettings() { )}
+ {/* Bouton test connexion IMAP */} +
+ +

Vérifie que la connexion au serveur IMAP est fonctionnelle (sans importer d'emails)

+
+
{emailServiceStatus?.isRunning ? ( <> diff --git a/drizzle/0027_complex_ultimo.sql b/drizzle/0027_complex_ultimo.sql new file mode 100644 index 0000000..3e80ba7 --- /dev/null +++ b/drizzle/0027_complex_ultimo.sql @@ -0,0 +1 @@ +ALTER TABLE `importSettings` ADD `emailImportAuthMode` enum('basic','oauth2') DEFAULT 'basic' NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0027_snapshot.json b/drizzle/meta/0027_snapshot.json new file mode 100644 index 0000000..dc680dc --- /dev/null +++ b/drizzle/meta/0027_snapshot.json @@ -0,0 +1,1832 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "9058cdd1-d372-462c-a13c-23b208e8f3b7", + "prevId": "fd2456f3-a6cc-4aad-94ec-26f7d0513fff", + "tables": { + "accountingAllocationList": { + "name": "accountingAllocationList", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "user_allocation_unique": { + "name": "user_allocation_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "accountingAllocationList_id": { + "name": "accountingAllocationList_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automationRules": { + "name": "automationRules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "isActive": { + "name": "isActive", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "priority": { + "name": "priority", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "conditions": { + "name": "conditions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conditionsLogic": { + "name": "conditionsLogic", + "type": "enum('AND','OR')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AND'" + }, + "actions": { + "name": "actions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "automationRules_id": { + "name": "automationRules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "bapHistory": { + "name": "bapHistory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceDate": { + "name": "invoiceDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeAchat": { + "name": "typeAchat", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "serviceConcerne": { + "name": "serviceConcerne", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ventilationComptable": { + "name": "ventilationComptable", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientName": { + "name": "recipientName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportMode": { + "name": "exportMode", + "type": "enum('browser','folder')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'browser'" + }, + "exportPath": { + "name": "exportPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pdfUrl": { + "name": "pdfUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureName": { + "name": "signatureName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadStatus": { + "name": "sharepointUploadStatus", + "type": "enum('success','error','skipped')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadPath": { + "name": "sharepointUploadPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadError": { + "name": "sharepointUploadError", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "validatedAt": { + "name": "validatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "bapHistory_id": { + "name": "bapHistory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "departmentList": { + "name": "departmentList", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "user_department_unique": { + "name": "user_department_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "departmentList_id": { + "name": "departmentList_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "importLogs": { + "name": "importLogs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalInvoicesDetected": { + "name": "totalInvoicesDetected", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "invoicesImported": { + "name": "invoicesImported", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "duplicatesIgnored": { + "name": "duplicatesIgnored", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "errors": { + "name": "errors", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "duplicateDetails": { + "name": "duplicateDetails", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "errorDetails": { + "name": "errorDetails", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "importedAt": { + "name": "importedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "importLogs_id": { + "name": "importLogs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "importSettings": { + "name": "importSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manualImportEnabled": { + "name": "manualImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "autoImportEnabled": { + "name": "autoImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "autoImportSourcePath": { + "name": "autoImportSourcePath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoImportFrequency": { + "name": "autoImportFrequency", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 60 + }, + "emailImportEnabled": { + "name": "emailImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "emailImportAddress": { + "name": "emailImportAddress", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportPassword": { + "name": "emailImportPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportHost": { + "name": "emailImportHost", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportPort": { + "name": "emailImportPort", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 993 + }, + "emailImportFrequency": { + "name": "emailImportFrequency", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "emailImportSinceDate": { + "name": "emailImportSinceDate", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportAuthMode": { + "name": "emailImportAuthMode", + "type": "enum('basic','oauth2')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'basic'" + }, + "exportFolder": { + "name": "exportFolder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportFolderType": { + "name": "exportFolderType", + "type": "enum('local','teams','sharepoint')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'local'" + }, + "bapExportMode": { + "name": "bapExportMode", + "type": "enum('browser','folder','both')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'browser'" + }, + "azureTenantId": { + "name": "azureTenantId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureClientId": { + "name": "azureClientId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureClientSecret": { + "name": "azureClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureSecretExpiresAt": { + "name": "azureSecretExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aiProvider": { + "name": "aiProvider", + "type": "enum('mistral','manus')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral'" + }, + "mistralApiKey": { + "name": "mistralApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiKey": { + "name": "manusForgeApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiUrl": { + "name": "manusForgeApiUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "importSettings_id": { + "name": "importSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "importSettings_userId_unique": { + "name": "importSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "invoiceLearnings": { + "name": "invoiceLearnings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierKey": { + "name": "supplierKey", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fieldName": { + "name": "fieldName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "originalValue": { + "name": "originalValue", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "correctedValue": { + "name": "correctedValue", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applyCount": { + "name": "applyCount", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invoiceLearnings_id": { + "name": "invoiceLearnings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invoices": { + "name": "invoices", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceIndexInFile": { + "name": "invoiceIndexInFile", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileKey": { + "name": "fileKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileUrl": { + "name": "fileUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceDate": { + "name": "invoiceDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deliveryNoteNumber": { + "name": "deliveryNoteNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "orderNumber": { + "name": "orderNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "decimal(10,2)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientName": { + "name": "recipientName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pageRange": { + "name": "pageRange", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qualityScore": { + "name": "qualityScore", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadataFileKey": { + "name": "metadataFileKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadataFileUrl": { + "name": "metadataFileUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('processing','completed','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'processing'" + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportStatus": { + "name": "exportStatus", + "type": "enum('not_exported','exported','export_error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'not_exported'" + }, + "manuallyEdited": { + "name": "manuallyEdited", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "serviceConcerne": { + "name": "serviceConcerne", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeAchat": { + "name": "typeAchat", + "type": "enum('CAPEX','OPEX')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ventilationComptable": { + "name": "ventilationComptable", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoFilledFields": { + "name": "autoFilledFields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extractedText": { + "name": "extractedText", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "isSubscription": { + "name": "isSubscription", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "bapValidated": { + "name": "bapValidated", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "bapValidatedAt": { + "name": "bapValidatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportedAt": { + "name": "exportedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportMode": { + "name": "exportMode", + "type": "enum('manual','automatic')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "supplier_invoice_date_unique": { + "name": "supplier_invoice_date_unique", + "columns": [ + "supplierName", + "invoiceNumber", + "invoiceDate" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invoices_id": { + "name": "invoices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llmFieldsConfig": { + "name": "llmFieldsConfig", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fieldName": { + "name": "fieldName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "displayName": { + "name": "displayName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "isRequired": { + "name": "isRequired", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "displayOrder": { + "name": "displayOrder", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llmFieldsConfig_id": { + "name": "llmFieldsConfig_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llmLogs": { + "name": "llmLogs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "operation": { + "name": "operation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "promptSent": { + "name": "promptSent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rawResponse": { + "name": "rawResponse", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanedResponse": { + "name": "cleanedResponse", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "processingTimeMs": { + "name": "processingTimeMs", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pageRange": { + "name": "pageRange", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llmLogs_id": { + "name": "llmLogs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "serviceSignatures": { + "name": "serviceSignatures", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "serviceName": { + "name": "serviceName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signatureId": { + "name": "signatureId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "user_service_unique": { + "name": "user_service_unique", + "columns": [ + "userId", + "serviceName" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "serviceSignatures_id": { + "name": "serviceSignatures_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "signatures": { + "name": "signatures", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "firstName": { + "name": "firstName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastName": { + "name": "lastName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imageKey": { + "name": "imageKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "signatures_id": { + "name": "signatures_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sourceFiles": { + "name": "sourceFiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileKey": { + "name": "fileKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileUrl": { + "name": "fileUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalInvoicesDetected": { + "name": "totalInvoicesDetected", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "processingStatus": { + "name": "processingStatus", + "type": "enum('processing','completed','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'processing'" + }, + "processingProgress": { + "name": "processingProgress", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sourceFiles_id": { + "name": "sourceFiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "userSettings": { + "name": "userSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "llmModel": { + "name": "llmModel", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral-large-latest'" + }, + "orderNumberFormat": { + "name": "orderNumberFormat", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumberKeywords": { + "name": "invoiceNumberKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deliveryNoteKeywords": { + "name": "deliveryNoteKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "orderNumberKeywords": { + "name": "orderNumberKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supplierKeywords": { + "name": "supplierKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmountKeywords": { + "name": "totalAmountKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscriptionKeywords": { + "name": "subscriptionKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientKeywords": { + "name": "recipientKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpRecipientFilter": { + "name": "sftpRecipientFilter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpHost": { + "name": "sftpHost", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpPort": { + "name": "sftpPort", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "sftpUsername": { + "name": "sftpUsername", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpPassword": { + "name": "sftpPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpRemotePath": { + "name": "sftpRemotePath", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/'" + }, + "sftpAutoExport": { + "name": "sftpAutoExport", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "llmLogsRetentionMonths": { + "name": "llmLogsRetentionMonths", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "learningConfidenceThreshold": { + "name": "learningConfidenceThreshold", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2 + }, + "aiProvider": { + "name": "aiProvider", + "type": "enum('mistral','manus')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral'" + }, + "mistralApiKey": { + "name": "mistralApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiKey": { + "name": "manusForgeApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiUrl": { + "name": "manusForgeApiUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "userSettings_id": { + "name": "userSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "userSettings_userId_unique": { + "name": "userSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "openId": { + "name": "openId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureAdId": { + "name": "azureAdId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "passwordHash": { + "name": "passwordHash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginMethod": { + "name": "loginMethod", + "type": "enum('manus','local','azure-ad')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('user','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "isActive": { + "name": "isActive", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "lastSignedIn": { + "name": "lastSignedIn", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_openId_unique": { + "name": "users_openId_unique", + "columns": [ + "openId" + ] + }, + "users_azureAdId_unique": { + "name": "users_azureAdId_unique", + "columns": [ + "azureAdId" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c52e217..ed0f036 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -190,6 +190,13 @@ "when": 1780411851758, "tag": "0026_perfect_richard_fisk", "breakpoints": true + }, + { + "idx": 27, + "version": "5", + "when": 1780653000245, + "tag": "0027_complex_ultimo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 88ef58a..38468fc 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -183,6 +183,7 @@ export const importSettings = mysqlTable("importSettings", { emailImportPort: int("emailImportPort").default(993), // IMAP port (default: 993 for SSL) emailImportFrequency: int("emailImportFrequency").default(30).notNull(), // Frequency in minutes (default: 30) emailImportSinceDate: int("emailImportSinceDate"), // Timestamp Unix (s) — ne pas lire les emails antérieurs à cette date + emailImportAuthMode: mysqlEnum("emailImportAuthMode", ["basic", "oauth2"]).default("basic").notNull(), // Auth mode: basic (login/password) or oauth2 (Azure AD token) // Export folder settings exportFolder: text("exportFolder"), // Path to folder for exporting invoices diff --git a/server/emailImportService.ts b/server/emailImportService.ts index f7b0570..a0419e9 100644 --- a/server/emailImportService.ts +++ b/server/emailImportService.ts @@ -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 { - return new Promise((resolve, reject) => { - const imap = new Imap({ +async function buildImapConfig(config: EmailImportConfig): Promise { + 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 { + // 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 { ); 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 { }); } +/** + * 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 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}` }; diff --git a/server/office365OAuth.ts b/server/office365OAuth.ts new file mode 100644 index 0000000..485bdce --- /dev/null +++ b/server/office365OAuth.ts @@ -0,0 +1,152 @@ +/** + * Office 365 OAuth2 IMAP Authentication Helper + * + * Microsoft a désactivé l'authentification basique (Basic Auth) pour Exchange Online + * depuis octobre 2022. Ce module implémente l'authentification OAuth2 via les + * credentials Azure AD (Client Credentials Flow) pour obtenir un token d'accès + * permettant de s'authentifier en IMAP via le mécanisme XOAUTH2. + * + * Prérequis côté Azure AD : + * - Application enregistrée avec les permissions : IMAP.AccessAsUser.All (ou IMAP.AccessAsApp) + * - Ou utiliser le flow "client_credentials" avec IMAP.AccessAsApp + * + * Référence : https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth + */ + +interface OAuth2TokenResponse { + access_token: string; + token_type: string; + expires_in: number; + scope?: string; + error?: string; + error_description?: string; +} + +/** + * Obtient un token OAuth2 pour IMAP via le flow Client Credentials + * (utilisé pour les applications de service sans interaction utilisateur) + * + * @param tenantId - Azure AD Tenant ID + * @param clientId - Azure AD Application (Client) ID + * @param clientSecret - Azure AD Client Secret + * @returns Access token string + */ +export async function getOffice365ImapToken( + tenantId: string, + clientId: string, + clientSecret: string +): Promise { + const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + + const params = new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + scope: "https://outlook.office365.com/.default", + grant_type: "client_credentials", + }); + + const response = await fetch(tokenUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params.toString(), + }); + + const data = (await response.json()) as OAuth2TokenResponse; + + if (!response.ok || data.error) { + throw new Error( + `OAuth2 token error: ${data.error || response.status} - ${ + data.error_description || response.statusText + }` + ); + } + + if (!data.access_token) { + throw new Error("OAuth2: no access_token in response"); + } + + return data.access_token; +} + +/** + * Génère la chaîne XOAUTH2 encodée en base64 pour l'authentification IMAP + * Format : base64("user=" + email + "\x01auth=Bearer " + token + "\x01\x01") + * + * @param email - Adresse email de la boîte à surveiller + * @param accessToken - Token OAuth2 obtenu via getOffice365ImapToken + * @returns Chaîne XOAUTH2 encodée en base64 + */ +export function buildXOAuth2String(email: string, accessToken: string): string { + const xoauth2 = `user=${email}\x01auth=Bearer ${accessToken}\x01\x01`; + return Buffer.from(xoauth2).toString("base64"); +} + +/** + * Teste la connexion IMAP OAuth2 en obtenant un token et en vérifiant + * que les credentials Azure AD sont valides. + * + * @param tenantId - Azure AD Tenant ID + * @param clientId - Azure AD Application (Client) ID + * @param clientSecret - Azure AD Client Secret + * @param emailAddress - Adresse email à tester + * @returns { success: boolean; message: string } + */ +export async function testOffice365ImapOAuth2( + tenantId: string, + clientId: string, + clientSecret: string, + emailAddress: string +): Promise<{ success: boolean; message: string; token?: string }> { + try { + if (!tenantId || !clientId || !clientSecret) { + return { + success: false, + message: + "Credentials Azure AD incomplets (Tenant ID, Client ID ou Client Secret manquant)", + }; + } + + const token = await getOffice365ImapToken(tenantId, clientId, clientSecret); + + return { + success: true, + message: `Token OAuth2 obtenu avec succès pour ${emailAddress}. L'authentification IMAP OAuth2 est configurée.`, + token, + }; + } catch (error: any) { + const msg = error.message || String(error); + + // Messages d'erreur plus clairs pour les cas courants + if (msg.includes("AADSTS700016") || msg.includes("application was not found")) { + return { + success: false, + message: `Client ID invalide ou application non trouvée dans le tenant Azure AD. Vérifiez le Client ID.`, + }; + } + if (msg.includes("AADSTS7000215") || msg.includes("Invalid client secret")) { + return { + success: false, + message: `Client Secret invalide ou expiré. Générez un nouveau secret dans Azure AD.`, + }; + } + if (msg.includes("AADSTS90002") || msg.includes("Tenant") || msg.includes("tenant")) { + return { + success: false, + message: `Tenant ID invalide. Vérifiez l'ID du tenant Azure AD.`, + }; + } + if (msg.includes("AADSTS65001") || msg.includes("consent")) { + return { + success: false, + message: `Permissions non accordées. L'application Azure AD nécessite le consentement admin pour IMAP.AccessAsApp.`, + }; + } + + return { + success: false, + message: `Erreur OAuth2 : ${msg}`, + }; + } +} diff --git a/server/routers.ts b/server/routers.ts index 19e73d6..6477918 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -82,7 +82,7 @@ import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtra import { localStoragePut, generateStorageKey } from "./localStorage"; import { testSftpConnection, exportInvoiceToSftp, getUserSftpConfig } from "./sftpExport"; import { drawBapCartouche } from "./bapCartouche"; -import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning, triggerEmailCheck } from "./emailImportService"; +import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning, triggerEmailCheck, testImapConnection } from "./emailImportService"; import { startFolderImportService, stopFolderImportService, isFolderImportServiceRunning } from "./folderImportService"; import { TRPCError } from "@trpc/server"; @@ -1567,6 +1567,7 @@ export const appRouter = router({ emailImportPort: z.number().min(1).max(65535).optional(), emailImportFrequency: z.number().min(1).optional(), emailImportSinceDate: z.number().nullable().optional(), // Unix timestamp (s) + emailImportAuthMode: z.enum(["basic", "oauth2"]).optional(), exportFolder: z.string().nullable().optional(), exportFolderType: z.enum(["local", "teams", "sharepoint"]).optional(), bapExportMode: z.enum(["browser", "folder", "both"]).optional(), @@ -1635,6 +1636,37 @@ export const appRouter = router({ } }), + testEmailConnection: protectedProcedure + .mutation(async ({ ctx }) => { + const settings = await getImportSettingsByUser(ctx.user.id); + if (!settings?.emailImportAddress || !settings?.emailImportHost) { + return { success: false, message: 'Configuration IMAP incomplète (adresse email ou serveur IMAP manquant)' }; + } + + const authMode = (settings as any).emailImportAuthMode as 'basic' | 'oauth2' || 'basic'; + + if (authMode === 'basic' && !settings.emailImportPassword) { + return { success: false, message: 'Mot de passe IMAP manquant' }; + } + if (authMode === 'oauth2' && (!settings.azureTenantId || !settings.azureClientId || !settings.azureClientSecret)) { + return { success: false, message: 'Credentials Azure AD incomplets pour OAuth2 (Tenant ID, Client ID, Client Secret requis)' }; + } + + const result = await testImapConnection({ + userId: ctx.user.id, + emailAddress: settings.emailImportAddress, + password: settings.emailImportPassword || '', + host: settings.emailImportHost, + port: settings.emailImportPort || 993, + authMode, + azureTenantId: settings.azureTenantId || undefined, + azureClientId: settings.azureClientId || undefined, + azureClientSecret: settings.azureClientSecret || undefined, + }); + + return result; + }), + testSharePointUpload: protectedProcedure .mutation(async ({ ctx }) => { const settings = await getImportSettingsByUser(ctx.user.id); diff --git a/todo.md b/todo.md index 4595e29..d1e7f30 100644 --- a/todo.md +++ b/todo.md @@ -631,3 +631,16 @@ - [ ] Log SharePoint dans l'historique BAP (colonnes sharepointUploadStatus + sharepointUploadPath) - [ ] Bouton "Tester la connexion" Azure AD dans les paramètres - [ ] Champ date d'expiration du secret Azure AD + alerte dans les paramètres + +## OAuth2 IMAP pour Office 365 +- [x] Créer server/office365OAuth.ts avec getOffice365ImapToken et buildXOAuth2String +- [x] Ajouter champ emailImportAuthMode (basic/oauth2) dans le schéma DB importSettings +- [x] Appliquer migration DB (pnpm db:push) +- [x] Modifier emailImportService.ts pour supporter OAuth2 XOAUTH2 via Azure AD +- [x] Ajouter fonction testImapConnection dans emailImportService.ts +- [x] Ajouter procédure testEmailConnection dans routers.ts +- [x] Ajouter emailImportAuthMode dans le schéma Zod de importSettings.update +- [x] Ajouter sélecteur mode auth (basic/oauth2) dans ImportSettings.tsx +- [x] Ajouter bouton "Tester la connexion IMAP" dans ImportSettings.tsx +- [ ] Déployer sur recette et production +- [ ] Appliquer migration emailImportAuthMode sur recette et production