+ {emailImportAuthMode === "basic" && (
+ )}
+ {emailImportAuthMode === "oauth2" && (
+
Serveur IMAP
@@ -475,6 +521,34 @@ export default function ImportSettings() {
)}
+ {/* Bouton test connexion IMAP */}
+
+
{
+ try {
+ toast.info("Test de connexion IMAP en cours...");
+ const result = await testEmailConnectionMutation.mutateAsync();
+ if (result.success) {
+ toast.success(result.message);
+ } else {
+ toast.error(result.message, { duration: 8000 });
+ }
+ } catch (e: any) {
+ toast.error("Erreur : " + (e?.message || "inconnue"));
+ }
+ }}
+ disabled={testEmailConnectionMutation.isPending}
+ className="border-blue-300 hover:bg-blue-50 dark:hover:bg-blue-950/20"
+ >
+ {testEmailConnectionMutation.isPending
+ ? <> Test en cours...>
+ : <> Tester la 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