Checkpoint: Checkpoint saved: Implémentation complète de l'import automatique par email.
Nouvelles fonctionnalités : ✅ Service d'import automatique par email (emailImportService.ts) ✅ Connexion IMAP avec support SSL/TLS ✅ Détection automatique des nouveaux emails non lus avec pièces jointes PDF ✅ Téléchargement et traitement des pièces jointes (même logique que l'upload manuel) ✅ Marquage des emails comme lus après traitement réussi ✅ Scheduler configurable (fréquence en minutes) ✅ Routes tRPC pour démarrer/arrêter/vérifier le statut du service ✅ Boutons de contrôle dans la page Paramètres de réception Fonctionnement : 1. L'utilisateur configure ses identifiants email dans "Paramètres de réception" 2. Il active l'import par email et configure la fréquence de vérification 3. Il clique sur "Démarrer le service" pour lancer la surveillance 4. Le service se connecte au serveur IMAP selon la fréquence configurée 5. Il détecte les nouveaux emails non lus avec pièces jointes PDF 6. Il télécharge chaque PDF et applique le même traitement que l'upload manuel : - Stockage local du fichier - Extraction avec Mistral AI - Détection de doublons - Création des factures en base de données - Génération des métadonnées JSON - Création du log d'import 7. Les emails traités sont marqués comme lus 8. Le service continue de tourner en arrière-plan selon la fréquence configurée Interface utilisateur : - Indicateur visuel "Service actif" avec point vert animé - Bouton "Démarrer le service" (désactivé si l'import email n'est pas activé) - Bouton "Arrêter le service" (rouge) pour stopper la surveillance - Messages toast pour confirmer le démarrage/arrêt Dépendances ajoutées : - imap : Client IMAP pour Node.js - mailparser : Parser d'emails avec support des pièces jointes - @types/imap et @types/mailparser : Types TypeScript Le service est maintenant prêt à être testé avec un vrai compte email IMAP.
This commit is contained in:
@@ -6,12 +6,15 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Loader2, Save, Upload, FolderOpen, Mail } from "lucide-react";
|
import { Loader2, Save, Upload, FolderOpen, Mail, Play, Square } from "lucide-react";
|
||||||
import DashboardLayout from "@/components/DashboardLayout";
|
import DashboardLayout from "@/components/DashboardLayout";
|
||||||
|
|
||||||
export default function ImportSettings() {
|
export default function ImportSettings() {
|
||||||
const { data: settings, isLoading } = trpc.importSettings.get.useQuery();
|
const { data: settings, isLoading } = trpc.importSettings.get.useQuery();
|
||||||
|
const { data: serviceStatus } = trpc.emailImportService.status.useQuery();
|
||||||
const updateMutation = trpc.importSettings.update.useMutation();
|
const updateMutation = trpc.importSettings.update.useMutation();
|
||||||
|
const startServiceMutation = trpc.emailImportService.start.useMutation();
|
||||||
|
const stopServiceMutation = trpc.emailImportService.stop.useMutation();
|
||||||
|
|
||||||
// Manual import
|
// Manual import
|
||||||
const [manualImportEnabled, setManualImportEnabled] = useState(true);
|
const [manualImportEnabled, setManualImportEnabled] = useState(true);
|
||||||
@@ -280,6 +283,70 @@ export default function ImportSettings() {
|
|||||||
Intervalle de temps entre chaque vérification des emails (minimum: 1 minute)
|
Intervalle de temps entre chaque vérification des emails (minimum: 1 minute)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Service Control Buttons */}
|
||||||
|
<div className="flex gap-2 pt-4 border-t">
|
||||||
|
{serviceStatus?.isRunning ? (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await stopServiceMutation.mutateAsync();
|
||||||
|
toast.success("Service arrêté", {
|
||||||
|
description: "L'import automatique par email a été arrêté.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Erreur", {
|
||||||
|
description: "Impossible d'arrêter le service.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={stopServiceMutation.isPending}
|
||||||
|
>
|
||||||
|
{stopServiceMutation.isPending ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Square className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Arrêter le service
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const result = await startServiceMutation.mutateAsync();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success("Service démarré", {
|
||||||
|
description: "L'import automatique par email est maintenant actif.",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error("Erreur", {
|
||||||
|
description: "Impossible de démarrer le service. Vérifiez la configuration.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Erreur", {
|
||||||
|
description: "Impossible de démarrer le service.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!emailImportEnabled || startServiceMutation.isPending}
|
||||||
|
>
|
||||||
|
{startServiceMutation.isPending ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Play className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Démarrer le service
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{serviceStatus?.isRunning && (
|
||||||
|
<span className="flex items-center text-sm text-green-600">
|
||||||
|
<span className="w-2 h-2 bg-green-600 rounded-full mr-2 animate-pulse" />
|
||||||
|
Service actif
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -48,7 +48,9 @@
|
|||||||
"@trpc/react-query": "^11.6.0",
|
"@trpc/react-query": "^11.6.0",
|
||||||
"@trpc/server": "^11.6.0",
|
"@trpc/server": "^11.6.0",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/imap": "^0.8.43",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/mailparser": "^3.4.6",
|
||||||
"@types/ssh2-sftp-client": "^9.0.6",
|
"@types/ssh2-sftp-client": "^9.0.6",
|
||||||
"axios": "^1.12.0",
|
"axios": "^1.12.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
@@ -62,10 +64,12 @@
|
|||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"framer-motion": "^12.23.22",
|
"framer-motion": "^12.23.22",
|
||||||
|
"imap": "^0.8.19",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"jose": "6.1.0",
|
"jose": "6.1.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
|
"mailparser": "^3.9.3",
|
||||||
"mysql2": "^3.15.0",
|
"mysql2": "^3.15.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
|||||||
267
pnpm-lock.yaml
generated
267
pnpm-lock.yaml
generated
@@ -121,9 +121,15 @@ importers:
|
|||||||
'@types/bcrypt':
|
'@types/bcrypt':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
|
'@types/imap':
|
||||||
|
specifier: ^0.8.43
|
||||||
|
version: 0.8.43
|
||||||
'@types/jsonwebtoken':
|
'@types/jsonwebtoken':
|
||||||
specifier: ^9.0.10
|
specifier: ^9.0.10
|
||||||
version: 9.0.10
|
version: 9.0.10
|
||||||
|
'@types/mailparser':
|
||||||
|
specifier: ^3.4.6
|
||||||
|
version: 3.4.6
|
||||||
'@types/ssh2-sftp-client':
|
'@types/ssh2-sftp-client':
|
||||||
specifier: ^9.0.6
|
specifier: ^9.0.6
|
||||||
version: 9.0.6
|
version: 9.0.6
|
||||||
@@ -163,6 +169,9 @@ importers:
|
|||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^12.23.22
|
specifier: ^12.23.22
|
||||||
version: 12.23.22(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
version: 12.23.22(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
||||||
|
imap:
|
||||||
|
specifier: ^0.8.19
|
||||||
|
version: 0.8.19
|
||||||
input-otp:
|
input-otp:
|
||||||
specifier: ^1.4.2
|
specifier: ^1.4.2
|
||||||
version: 1.4.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
version: 1.4.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
||||||
@@ -175,6 +184,9 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.453.0
|
specifier: ^0.453.0
|
||||||
version: 0.453.0(react@19.2.1)
|
version: 0.453.0(react@19.2.1)
|
||||||
|
mailparser:
|
||||||
|
specifier: ^3.9.3
|
||||||
|
version: 3.9.3
|
||||||
mysql2:
|
mysql2:
|
||||||
specifier: ^3.15.0
|
specifier: ^3.15.0
|
||||||
version: 3.15.1
|
version: 3.15.1
|
||||||
@@ -1894,6 +1906,9 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
|
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||||
|
|
||||||
'@shikijs/core@3.14.0':
|
'@shikijs/core@3.14.0':
|
||||||
resolution: {integrity: sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==}
|
resolution: {integrity: sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==}
|
||||||
|
|
||||||
@@ -2399,12 +2414,18 @@ packages:
|
|||||||
'@types/http-errors@2.0.5':
|
'@types/http-errors@2.0.5':
|
||||||
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
||||||
|
|
||||||
|
'@types/imap@0.8.43':
|
||||||
|
resolution: {integrity: sha512-POPoqrDax9mxM2N4ITZYCWaFtg1ORVfzJe4S7xwSh9aHawdEb7FwWTJYiAhzIvWp7DM+6BajnzYOwZ1BUrqtow==}
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.10':
|
'@types/jsonwebtoken@9.0.10':
|
||||||
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||||
|
|
||||||
'@types/katex@0.16.7':
|
'@types/katex@0.16.7':
|
||||||
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||||
|
|
||||||
|
'@types/mailparser@3.4.6':
|
||||||
|
resolution: {integrity: sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==}
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
||||||
|
|
||||||
@@ -2496,6 +2517,9 @@ packages:
|
|||||||
'@vitest/utils@2.1.9':
|
'@vitest/utils@2.1.9':
|
||||||
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
|
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
|
||||||
|
|
||||||
|
'@zone-eu/mailsplit@5.4.8':
|
||||||
|
resolution: {integrity: sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==}
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -2705,6 +2729,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
|
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
|
||||||
engines: {node: '>=12.13'}
|
engines: {node: '>=12.13'}
|
||||||
|
|
||||||
|
core-util-is@1.0.3:
|
||||||
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
cose-base@1.0.3:
|
cose-base@1.0.3:
|
||||||
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
||||||
|
|
||||||
@@ -2920,6 +2947,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
deepmerge@4.3.1:
|
||||||
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
delaunator@5.0.1:
|
delaunator@5.0.1:
|
||||||
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
|
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
|
||||||
|
|
||||||
@@ -2956,9 +2987,22 @@ packages:
|
|||||||
dom-helpers@5.2.1:
|
dom-helpers@5.2.1:
|
||||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||||
|
|
||||||
|
dom-serializer@2.0.0:
|
||||||
|
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||||
|
|
||||||
|
domelementtype@2.3.0:
|
||||||
|
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||||
|
|
||||||
|
domhandler@5.0.3:
|
||||||
|
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
dompurify@3.3.0:
|
dompurify@3.3.0:
|
||||||
resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==}
|
resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==}
|
||||||
|
|
||||||
|
domutils@3.2.2:
|
||||||
|
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||||
|
|
||||||
dotenv@17.2.3:
|
dotenv@17.2.3:
|
||||||
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -3093,10 +3137,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
encoding-japanese@2.2.0:
|
||||||
|
resolution: {integrity: sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==}
|
||||||
|
engines: {node: '>=8.10.0'}
|
||||||
|
|
||||||
enhanced-resolve@5.18.3:
|
enhanced-resolve@5.18.3:
|
||||||
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
|
|
||||||
|
entities@4.5.0:
|
||||||
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
entities@6.0.1:
|
entities@6.0.1:
|
||||||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
@@ -3339,12 +3391,23 @@ packages:
|
|||||||
hastscript@9.0.1:
|
hastscript@9.0.1:
|
||||||
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
|
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
|
||||||
|
|
||||||
|
he@1.2.0:
|
||||||
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
html-to-text@9.0.5:
|
||||||
|
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
html-url-attributes@3.0.1:
|
html-url-attributes@3.0.1:
|
||||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||||
|
|
||||||
html-void-elements@3.0.0:
|
html-void-elements@3.0.0:
|
||||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||||
|
|
||||||
|
htmlparser2@8.0.2:
|
||||||
|
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||||
|
|
||||||
http-errors@2.0.0:
|
http-errors@2.0.0:
|
||||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -3361,6 +3424,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
|
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
iconv-lite@0.7.2:
|
||||||
|
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
imap@0.8.19:
|
||||||
|
resolution: {integrity: sha512-z5DxEA1uRnZG73UcPA4ES5NSCGnPuuouUx43OPX7KZx1yzq3N8/vx2mtXEShT5inxB3pRgnfG1hijfu7XN2YMw==}
|
||||||
|
engines: {node: '>=0.8.0'}
|
||||||
|
|
||||||
inherits@2.0.4:
|
inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
@@ -3407,6 +3478,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
|
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
|
||||||
engines: {node: '>=12.13'}
|
engines: {node: '>=12.13'}
|
||||||
|
|
||||||
|
isarray@0.0.1:
|
||||||
|
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
|
||||||
|
|
||||||
jiti@2.6.1:
|
jiti@2.6.1:
|
||||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -3457,6 +3531,18 @@ packages:
|
|||||||
layout-base@2.0.1:
|
layout-base@2.0.1:
|
||||||
resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
|
resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
|
||||||
|
|
||||||
|
leac@0.6.0:
|
||||||
|
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
|
||||||
|
|
||||||
|
libbase64@1.3.0:
|
||||||
|
resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==}
|
||||||
|
|
||||||
|
libmime@5.3.7:
|
||||||
|
resolution: {integrity: sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==}
|
||||||
|
|
||||||
|
libqp@2.1.1:
|
||||||
|
resolution: {integrity: sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==}
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.1:
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -3521,6 +3607,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
linkify-it@5.0.0:
|
||||||
|
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||||
|
|
||||||
local-pkg@1.1.2:
|
local-pkg@1.1.2:
|
||||||
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
|
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -3589,6 +3678,9 @@ packages:
|
|||||||
magic-string@0.30.19:
|
magic-string@0.30.19:
|
||||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
||||||
|
|
||||||
|
mailparser@3.9.3:
|
||||||
|
resolution: {integrity: sha512-AnB0a3zROum6fLaa52L+/K2SoRJVyFDk78Ea6q1D0ofcZLxWEWDtsS1+OrVqKbV7r5dulKL/AwYQccFGAPpuYQ==}
|
||||||
|
|
||||||
make-cancellable-promise@2.0.0:
|
make-cancellable-promise@2.0.0:
|
||||||
resolution: {integrity: sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==}
|
resolution: {integrity: sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==}
|
||||||
|
|
||||||
@@ -3848,6 +3940,10 @@ packages:
|
|||||||
node-releases@2.0.23:
|
node-releases@2.0.23:
|
||||||
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
|
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
|
||||||
|
|
||||||
|
nodemailer@7.0.13:
|
||||||
|
resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
normalize-range@0.1.2:
|
normalize-range@0.1.2:
|
||||||
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3882,6 +3978,9 @@ packages:
|
|||||||
parse5@7.3.0:
|
parse5@7.3.0:
|
||||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||||
|
|
||||||
|
parseley@0.12.1:
|
||||||
|
resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
|
||||||
|
|
||||||
parseurl@1.3.3:
|
parseurl@1.3.3:
|
||||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -3909,6 +4008,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==}
|
resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==}
|
||||||
engines: {node: '>=20.16.0 || >=22.3.0'}
|
engines: {node: '>=20.16.0 || >=22.3.0'}
|
||||||
|
|
||||||
|
peberminta@0.9.0:
|
||||||
|
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -3965,6 +4067,10 @@ packages:
|
|||||||
proxy-from-env@1.1.0:
|
proxy-from-env@1.1.0:
|
||||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
|
||||||
|
punycode.js@2.3.1:
|
||||||
|
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
qs@6.13.0:
|
qs@6.13.0:
|
||||||
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
@@ -4075,6 +4181,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==}
|
resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
readable-stream@1.1.14:
|
||||||
|
resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==}
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
readable-stream@3.6.2:
|
||||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -4152,6 +4261,13 @@ packages:
|
|||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
|
selderee@0.11.0:
|
||||||
|
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
|
||||||
|
|
||||||
|
semver@5.3.0:
|
||||||
|
resolution: {integrity: sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
semver@6.3.1:
|
semver@6.3.1:
|
||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -4248,6 +4364,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.0.0 || ^19.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
string_decoder@0.10.31:
|
||||||
|
resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
string_decoder@1.3.0:
|
||||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
|
|
||||||
@@ -4317,6 +4436,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
tlds@1.261.0:
|
||||||
|
resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
toidentifier@1.0.1:
|
toidentifier@1.0.1:
|
||||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
@@ -4360,6 +4483,9 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
uc.micro@2.1.0:
|
||||||
|
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||||
|
|
||||||
ufo@1.6.1:
|
ufo@1.6.1:
|
||||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
||||||
|
|
||||||
@@ -4428,6 +4554,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
utf7@1.0.2:
|
||||||
|
resolution: {integrity: sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw==}
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@@ -6348,6 +6477,11 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.52.4':
|
'@rollup/rollup-win32-x64-msvc@4.52.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
|
dependencies:
|
||||||
|
domhandler: 5.0.3
|
||||||
|
selderee: 0.11.0
|
||||||
|
|
||||||
'@shikijs/core@3.14.0':
|
'@shikijs/core@3.14.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@shikijs/types': 3.14.0
|
'@shikijs/types': 3.14.0
|
||||||
@@ -7006,6 +7140,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/http-errors@2.0.5': {}
|
'@types/http-errors@2.0.5': {}
|
||||||
|
|
||||||
|
'@types/imap@0.8.43':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.7.0
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.10':
|
'@types/jsonwebtoken@9.0.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
@@ -7013,6 +7151,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/katex@0.16.7': {}
|
'@types/katex@0.16.7': {}
|
||||||
|
|
||||||
|
'@types/mailparser@3.4.6':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.7.0
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
@@ -7125,6 +7268,12 @@ snapshots:
|
|||||||
loupe: 3.2.1
|
loupe: 3.2.1
|
||||||
tinyrainbow: 1.2.0
|
tinyrainbow: 1.2.0
|
||||||
|
|
||||||
|
'@zone-eu/mailsplit@5.4.8':
|
||||||
|
dependencies:
|
||||||
|
libbase64: 1.3.0
|
||||||
|
libmime: 5.3.7
|
||||||
|
libqp: 2.1.1
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
@@ -7333,6 +7482,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-what: 4.1.16
|
is-what: 4.1.16
|
||||||
|
|
||||||
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
cose-base@1.0.3:
|
cose-base@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
layout-base: 1.0.2
|
layout-base: 1.0.2
|
||||||
@@ -7559,6 +7710,8 @@ snapshots:
|
|||||||
|
|
||||||
deep-eql@5.0.2: {}
|
deep-eql@5.0.2: {}
|
||||||
|
|
||||||
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
delaunator@5.0.1:
|
delaunator@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
robust-predicates: 3.0.2
|
robust-predicates: 3.0.2
|
||||||
@@ -7586,10 +7739,28 @@ snapshots:
|
|||||||
'@babel/runtime': 7.28.4
|
'@babel/runtime': 7.28.4
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
|
dom-serializer@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
entities: 4.5.0
|
||||||
|
|
||||||
|
domelementtype@2.3.0: {}
|
||||||
|
|
||||||
|
domhandler@5.0.3:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
|
||||||
dompurify@3.3.0:
|
dompurify@3.3.0:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/trusted-types': 2.0.7
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
|
domutils@3.2.2:
|
||||||
|
dependencies:
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
|
||||||
dotenv@17.2.3: {}
|
dotenv@17.2.3: {}
|
||||||
|
|
||||||
drizzle-kit@0.31.5:
|
drizzle-kit@0.31.5:
|
||||||
@@ -7635,11 +7806,15 @@ snapshots:
|
|||||||
|
|
||||||
encodeurl@2.0.0: {}
|
encodeurl@2.0.0: {}
|
||||||
|
|
||||||
|
encoding-japanese@2.2.0: {}
|
||||||
|
|
||||||
enhanced-resolve@5.18.3:
|
enhanced-resolve@5.18.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
tapable: 2.3.0
|
tapable: 2.3.0
|
||||||
|
|
||||||
|
entities@4.5.0: {}
|
||||||
|
|
||||||
entities@6.0.1: {}
|
entities@6.0.1: {}
|
||||||
|
|
||||||
es-define-property@1.0.1: {}
|
es-define-property@1.0.1: {}
|
||||||
@@ -8028,10 +8203,27 @@ snapshots:
|
|||||||
property-information: 7.1.0
|
property-information: 7.1.0
|
||||||
space-separated-tokens: 2.0.2
|
space-separated-tokens: 2.0.2
|
||||||
|
|
||||||
|
he@1.2.0: {}
|
||||||
|
|
||||||
|
html-to-text@9.0.5:
|
||||||
|
dependencies:
|
||||||
|
'@selderee/plugin-htmlparser2': 0.11.0
|
||||||
|
deepmerge: 4.3.1
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
htmlparser2: 8.0.2
|
||||||
|
selderee: 0.11.0
|
||||||
|
|
||||||
html-url-attributes@3.0.1: {}
|
html-url-attributes@3.0.1: {}
|
||||||
|
|
||||||
html-void-elements@3.0.0: {}
|
html-void-elements@3.0.0: {}
|
||||||
|
|
||||||
|
htmlparser2@8.0.2:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.2.2
|
||||||
|
entities: 4.5.0
|
||||||
|
|
||||||
http-errors@2.0.0:
|
http-errors@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
@@ -8052,6 +8244,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
iconv-lite@0.7.2:
|
||||||
|
dependencies:
|
||||||
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
imap@0.8.19:
|
||||||
|
dependencies:
|
||||||
|
readable-stream: 1.1.14
|
||||||
|
utf7: 1.0.2
|
||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
inline-style-parser@0.2.4: {}
|
inline-style-parser@0.2.4: {}
|
||||||
@@ -8084,6 +8285,8 @@ snapshots:
|
|||||||
|
|
||||||
is-what@4.1.16: {}
|
is-what@4.1.16: {}
|
||||||
|
|
||||||
|
isarray@0.0.1: {}
|
||||||
|
|
||||||
jiti@2.6.1: {}
|
jiti@2.6.1: {}
|
||||||
|
|
||||||
jose@6.1.0: {}
|
jose@6.1.0: {}
|
||||||
@@ -8138,6 +8341,19 @@ snapshots:
|
|||||||
|
|
||||||
layout-base@2.0.1: {}
|
layout-base@2.0.1: {}
|
||||||
|
|
||||||
|
leac@0.6.0: {}
|
||||||
|
|
||||||
|
libbase64@1.3.0: {}
|
||||||
|
|
||||||
|
libmime@5.3.7:
|
||||||
|
dependencies:
|
||||||
|
encoding-japanese: 2.2.0
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
libbase64: 1.3.0
|
||||||
|
libqp: 2.1.1
|
||||||
|
|
||||||
|
libqp@2.1.1: {}
|
||||||
|
|
||||||
lightningcss-darwin-arm64@1.30.1:
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -8183,6 +8399,10 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.30.1
|
lightningcss-win32-arm64-msvc: 1.30.1
|
||||||
lightningcss-win32-x64-msvc: 1.30.1
|
lightningcss-win32-x64-msvc: 1.30.1
|
||||||
|
|
||||||
|
linkify-it@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
uc.micro: 2.1.0
|
||||||
|
|
||||||
local-pkg@1.1.2:
|
local-pkg@1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
@@ -8237,6 +8457,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
mailparser@3.9.3:
|
||||||
|
dependencies:
|
||||||
|
'@zone-eu/mailsplit': 5.4.8
|
||||||
|
encoding-japanese: 2.2.0
|
||||||
|
he: 1.2.0
|
||||||
|
html-to-text: 9.0.5
|
||||||
|
iconv-lite: 0.7.2
|
||||||
|
libmime: 5.3.7
|
||||||
|
linkify-it: 5.0.0
|
||||||
|
nodemailer: 7.0.13
|
||||||
|
punycode.js: 2.3.1
|
||||||
|
tlds: 1.261.0
|
||||||
|
|
||||||
make-cancellable-promise@2.0.0: {}
|
make-cancellable-promise@2.0.0: {}
|
||||||
|
|
||||||
make-event-props@2.0.0: {}
|
make-event-props@2.0.0: {}
|
||||||
@@ -8719,6 +8952,8 @@ snapshots:
|
|||||||
|
|
||||||
node-releases@2.0.23: {}
|
node-releases@2.0.23: {}
|
||||||
|
|
||||||
|
nodemailer@7.0.13: {}
|
||||||
|
|
||||||
normalize-range@0.1.2: {}
|
normalize-range@0.1.2: {}
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
@@ -8755,6 +8990,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
entities: 6.0.1
|
entities: 6.0.1
|
||||||
|
|
||||||
|
parseley@0.12.1:
|
||||||
|
dependencies:
|
||||||
|
leac: 0.6.0
|
||||||
|
peberminta: 0.9.0
|
||||||
|
|
||||||
parseurl@1.3.3: {}
|
parseurl@1.3.3: {}
|
||||||
|
|
||||||
path-data-parser@0.1.0: {}
|
path-data-parser@0.1.0: {}
|
||||||
@@ -8778,6 +9018,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas': 0.1.88
|
'@napi-rs/canvas': 0.1.88
|
||||||
|
|
||||||
|
peberminta@0.9.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.3: {}
|
||||||
@@ -8835,6 +9077,8 @@ snapshots:
|
|||||||
|
|
||||||
proxy-from-env@1.1.0: {}
|
proxy-from-env@1.1.0: {}
|
||||||
|
|
||||||
|
punycode.js@2.3.1: {}
|
||||||
|
|
||||||
qs@6.13.0:
|
qs@6.13.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
@@ -8956,6 +9200,13 @@ snapshots:
|
|||||||
|
|
||||||
react@19.2.1: {}
|
react@19.2.1: {}
|
||||||
|
|
||||||
|
readable-stream@1.1.14:
|
||||||
|
dependencies:
|
||||||
|
core-util-is: 1.0.3
|
||||||
|
inherits: 2.0.4
|
||||||
|
isarray: 0.0.1
|
||||||
|
string_decoder: 0.10.31
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
readable-stream@3.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
@@ -9099,6 +9350,12 @@ snapshots:
|
|||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
|
selderee@0.11.0:
|
||||||
|
dependencies:
|
||||||
|
parseley: 0.12.1
|
||||||
|
|
||||||
|
semver@5.3.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
semver@7.7.3: {}
|
semver@7.7.3: {}
|
||||||
@@ -9236,6 +9493,8 @@ snapshots:
|
|||||||
- '@types/react'
|
- '@types/react'
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
string_decoder@0.10.31: {}
|
||||||
|
|
||||||
string_decoder@1.3.0:
|
string_decoder@1.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
@@ -9298,6 +9557,8 @@ snapshots:
|
|||||||
|
|
||||||
tinyspy@3.0.2: {}
|
tinyspy@3.0.2: {}
|
||||||
|
|
||||||
|
tlds@1.261.0: {}
|
||||||
|
|
||||||
toidentifier@1.0.1: {}
|
toidentifier@1.0.1: {}
|
||||||
|
|
||||||
trim-lines@3.0.1: {}
|
trim-lines@3.0.1: {}
|
||||||
@@ -9330,6 +9591,8 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
uc.micro@2.1.0: {}
|
||||||
|
|
||||||
ufo@1.6.1: {}
|
ufo@1.6.1: {}
|
||||||
|
|
||||||
undici-types@5.26.5: {}
|
undici-types@5.26.5: {}
|
||||||
@@ -9406,6 +9669,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.1
|
react: 19.2.1
|
||||||
|
|
||||||
|
utf7@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
semver: 5.3.0
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
utils-merge@1.0.1: {}
|
utils-merge@1.0.1: {}
|
||||||
|
|||||||
427
server/emailImportService.ts
Normal file
427
server/emailImportService.ts
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
import Imap from "imap";
|
||||||
|
import { simpleParser, ParsedMail, Attachment } from "mailparser";
|
||||||
|
import {
|
||||||
|
getImportSettingsByUser,
|
||||||
|
createSourceFile,
|
||||||
|
updateSourceFile,
|
||||||
|
getUserSettings,
|
||||||
|
findDuplicateInvoice,
|
||||||
|
createInvoice,
|
||||||
|
createImportLog,
|
||||||
|
} from "./db";
|
||||||
|
import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor";
|
||||||
|
import { localStoragePut, generateStorageKey } from "./localStorage";
|
||||||
|
|
||||||
|
interface EmailImportConfig {
|
||||||
|
userId: number;
|
||||||
|
emailAddress: string;
|
||||||
|
password: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store active intervals for each user
|
||||||
|
const activeIntervals = new Map<number, NodeJS.Timeout>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a single email attachment (PDF)
|
||||||
|
* Replicates the same logic as manual upload
|
||||||
|
*/
|
||||||
|
async function processEmailAttachment(
|
||||||
|
userId: number,
|
||||||
|
attachment: Attachment,
|
||||||
|
emailSubject: string
|
||||||
|
): Promise<void> {
|
||||||
|
const fileName = attachment.filename || `email-attachment-${Date.now()}.pdf`;
|
||||||
|
console.log(`[EmailImport] Processing attachment: ${fileName} from email: ${emailSubject}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert attachment content to Buffer
|
||||||
|
const fileBuffer = attachment.content;
|
||||||
|
console.log(`[EmailImport] File size: ${fileBuffer.length} bytes`);
|
||||||
|
|
||||||
|
// Store source file
|
||||||
|
const sourceFileKey = generateStorageKey(userId, fileName);
|
||||||
|
console.log(`[EmailImport] Generated storage key: ${sourceFileKey}`);
|
||||||
|
|
||||||
|
let sourceFileUrl: string;
|
||||||
|
try {
|
||||||
|
const result = await localStoragePut(sourceFileKey, fileBuffer, "application/pdf");
|
||||||
|
sourceFileUrl = result.url;
|
||||||
|
console.log(`[EmailImport] File stored successfully at: ${sourceFileUrl}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[EmailImport] FAILED to store file:`, error);
|
||||||
|
throw new Error("Failed to store PDF file");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create source file record
|
||||||
|
const sourceFile = await createSourceFile({
|
||||||
|
userId,
|
||||||
|
fileName,
|
||||||
|
fileKey: sourceFileKey,
|
||||||
|
fileUrl: sourceFileUrl,
|
||||||
|
processingStatus: "processing",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[EmailImport] Source file record created with ID: ${sourceFile.id}`);
|
||||||
|
|
||||||
|
// Get user settings for custom keywords
|
||||||
|
const settings = await getUserSettings(userId);
|
||||||
|
const customKeywords = settings ? {
|
||||||
|
invoiceNumber: settings.invoiceNumberKeywords,
|
||||||
|
deliveryNote: settings.deliveryNoteKeywords,
|
||||||
|
orderNumber: settings.orderNumberKeywords,
|
||||||
|
supplier: settings.supplierKeywords,
|
||||||
|
totalAmount: settings.totalAmountKeywords,
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
|
const model = settings?.llmModel || "mistral-large-latest";
|
||||||
|
|
||||||
|
// Extract invoices
|
||||||
|
console.log(`[EmailImport] Starting invoice extraction...`);
|
||||||
|
const result = await extractInvoicesWithMistral(
|
||||||
|
fileBuffer,
|
||||||
|
userId,
|
||||||
|
sourceFile.id,
|
||||||
|
model,
|
||||||
|
customKeywords
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[EmailImport] Extraction complete: ${result.invoiceCount} invoice(s) detected`);
|
||||||
|
|
||||||
|
// Update source file with total count
|
||||||
|
await updateSourceFile(sourceFile.id, {
|
||||||
|
totalInvoicesDetected: result.invoiceCount,
|
||||||
|
processingProgress: `Extraction ${result.invoiceCount} facture(s) détectée(s)`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process each invoice
|
||||||
|
let importedCount = 0;
|
||||||
|
let duplicatesCount = 0;
|
||||||
|
let errorsCount = 0;
|
||||||
|
const duplicateDetails: any[] = [];
|
||||||
|
const errorDetails: any[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < result.invoices.length; i++) {
|
||||||
|
const invoiceData = result.invoices[i]!;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update progress
|
||||||
|
await updateSourceFile(sourceFile.id, {
|
||||||
|
processingProgress: `Extraction ${i + 1}/${result.invoiceCount} factures...`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for duplicates
|
||||||
|
const duplicate = await findDuplicateInvoice(
|
||||||
|
invoiceData.supplierName,
|
||||||
|
invoiceData.invoiceNumber,
|
||||||
|
invoiceData.invoiceDate
|
||||||
|
);
|
||||||
|
|
||||||
|
if (duplicate) {
|
||||||
|
duplicatesCount++;
|
||||||
|
duplicateDetails.push({
|
||||||
|
supplierName: invoiceData.supplierName,
|
||||||
|
invoiceNumber: invoiceData.invoiceNumber,
|
||||||
|
invoiceDate: invoiceData.invoiceDate,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate metadata JSON
|
||||||
|
const metadataJson = generateMetadataJSON(invoiceData);
|
||||||
|
const metadataKey = generateStorageKey(userId, `${fileName}-${i + 1}-metadata.json`);
|
||||||
|
const { url: metadataUrl } = await localStoragePut(
|
||||||
|
metadataKey,
|
||||||
|
Buffer.from(metadataJson),
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create invoice record
|
||||||
|
await createInvoice({
|
||||||
|
userId,
|
||||||
|
sourceFileId: sourceFile.id,
|
||||||
|
invoiceIndexInFile: i + 1,
|
||||||
|
fileName: `${fileName} - Facture ${i + 1}`,
|
||||||
|
fileKey: sourceFileKey,
|
||||||
|
fileUrl: sourceFileUrl,
|
||||||
|
supplierName: invoiceData.supplierName,
|
||||||
|
invoiceNumber: invoiceData.invoiceNumber,
|
||||||
|
invoiceDate: invoiceData.invoiceDate,
|
||||||
|
deliveryNoteNumber: invoiceData.deliveryNoteNumber,
|
||||||
|
orderNumber: invoiceData.orderNumber,
|
||||||
|
totalAmount: invoiceData.totalAmount?.toString(),
|
||||||
|
pageRange: invoiceData.pageRange,
|
||||||
|
qualityScore: invoiceData.qualityScore,
|
||||||
|
metadataFileKey: metadataKey,
|
||||||
|
metadataFileUrl: metadataUrl,
|
||||||
|
status: "completed",
|
||||||
|
});
|
||||||
|
|
||||||
|
importedCount++;
|
||||||
|
} catch (error: any) {
|
||||||
|
errorsCount++;
|
||||||
|
errorDetails.push({
|
||||||
|
invoiceIndex: i + 1,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update source file status
|
||||||
|
await updateSourceFile(sourceFile.id, {
|
||||||
|
processingStatus: "completed",
|
||||||
|
processingProgress: `Terminé: ${importedCount} importée(s), ${duplicatesCount} doublon(s)`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create import log
|
||||||
|
await createImportLog({
|
||||||
|
userId,
|
||||||
|
sourceFileId: sourceFile.id,
|
||||||
|
fileName,
|
||||||
|
totalInvoicesDetected: result.invoiceCount,
|
||||||
|
invoicesImported: importedCount,
|
||||||
|
duplicatesIgnored: duplicatesCount,
|
||||||
|
errors: errorsCount,
|
||||||
|
duplicateDetails: duplicateDetails.length > 0 ? JSON.stringify(duplicateDetails) : null,
|
||||||
|
errorDetails: errorDetails.length > 0 ? JSON.stringify(errorDetails) : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[EmailImport] Successfully processed attachment: ${fileName}`);
|
||||||
|
console.log(`[EmailImport] Results: ${importedCount} imported, ${duplicatesCount} duplicates, ${errorsCount} errors`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[EmailImport] Error processing attachment ${attachment.filename}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to IMAP and process unread emails with PDF attachments
|
||||||
|
*/
|
||||||
|
async function checkEmailsForPDFs(config: EmailImportConfig): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const imap = new Imap({
|
||||||
|
user: config.emailAddress,
|
||||||
|
password: config.password,
|
||||||
|
host: config.host,
|
||||||
|
port: config.port,
|
||||||
|
tls: true,
|
||||||
|
tlsOptions: { rejectUnauthorized: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
openInbox((err, box) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("[EmailImport] Error opening inbox:", err);
|
||||||
|
imap.end();
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for unread emails
|
||||||
|
imap.search(["UNSEEN"], (err, results) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("[EmailImport] Error searching emails:", err);
|
||||||
|
imap.end();
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!results || results.length === 0) {
|
||||||
|
console.log(`[EmailImport] No unread emails found for user ${config.userId}`);
|
||||||
|
imap.end();
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[EmailImport] Found ${results.length} unread emails for user ${config.userId}`);
|
||||||
|
|
||||||
|
const fetch = imap.fetch(results, {
|
||||||
|
bodies: "",
|
||||||
|
markSeen: false, // Don't mark as seen yet
|
||||||
|
});
|
||||||
|
|
||||||
|
const processedEmails: number[] = [];
|
||||||
|
|
||||||
|
fetch.on("message", (msg, seqno) => {
|
||||||
|
msg.on("body", (stream) => {
|
||||||
|
simpleParser(stream as any, async (err, parsed: ParsedMail) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("[EmailImport] Error parsing email:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email has PDF attachments
|
||||||
|
const pdfAttachments = parsed.attachments.filter(
|
||||||
|
(att) =>
|
||||||
|
att.contentType === "application/pdf" ||
|
||||||
|
att.filename?.toLowerCase().endsWith(".pdf")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pdfAttachments.length === 0) {
|
||||||
|
console.log(`[EmailImport] Email ${seqno} has no PDF attachments, skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[EmailImport] Email ${seqno} has ${pdfAttachments.length} PDF attachment(s)`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process each PDF attachment
|
||||||
|
for (const attachment of pdfAttachments) {
|
||||||
|
try {
|
||||||
|
await processEmailAttachment(
|
||||||
|
config.userId,
|
||||||
|
attachment,
|
||||||
|
parsed.subject || "No subject"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mark this email as successfully processed
|
||||||
|
if (!processedEmails.includes(seqno)) {
|
||||||
|
processedEmails.push(seqno);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`[EmailImport] Failed to process attachment from email ${seqno}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch.once("error", (err) => {
|
||||||
|
console.error("[EmailImport] Fetch error:", err);
|
||||||
|
imap.end();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch.once("end", () => {
|
||||||
|
console.log(`[EmailImport] Finished fetching emails for user ${config.userId}`);
|
||||||
|
|
||||||
|
// Mark successfully processed emails as seen
|
||||||
|
if (processedEmails.length > 0) {
|
||||||
|
imap.addFlags(processedEmails, ["\\Seen"], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("[EmailImport] Error marking emails as seen:", err);
|
||||||
|
} else {
|
||||||
|
console.log(`[EmailImport] Marked ${processedEmails.length} emails as seen`);
|
||||||
|
}
|
||||||
|
imap.end();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
imap.end();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
imap.once("error", (err) => {
|
||||||
|
console.error("[EmailImport] IMAP connection error:", err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
imap.once("end", () => {
|
||||||
|
console.log(`[EmailImport] IMAP connection ended for user ${config.userId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
imap.connect();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start email import service for a user
|
||||||
|
*/
|
||||||
|
export async function startEmailImportService(userId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Get user's import settings
|
||||||
|
const settings = await getImportSettingsByUser(userId);
|
||||||
|
|
||||||
|
if (!settings || settings.emailImportEnabled !== 1) {
|
||||||
|
console.log(`[EmailImport] Email import not enabled for user ${userId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.emailImportAddress || !settings.emailImportPassword || !settings.emailImportHost) {
|
||||||
|
console.log(`[EmailImport] Email import configuration incomplete for user ${userId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop existing service if running
|
||||||
|
stopEmailImportService(userId);
|
||||||
|
|
||||||
|
const config: EmailImportConfig = {
|
||||||
|
userId,
|
||||||
|
emailAddress: settings.emailImportAddress,
|
||||||
|
password: settings.emailImportPassword,
|
||||||
|
host: settings.emailImportHost,
|
||||||
|
port: settings.emailImportPort || 993,
|
||||||
|
};
|
||||||
|
|
||||||
|
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`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run immediately on start
|
||||||
|
checkEmailsForPDFs(config).catch((error) => {
|
||||||
|
console.error(`[EmailImport] Error checking emails for user ${userId}:`, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up interval for periodic checks
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
checkEmailsForPDFs(config).catch((error) => {
|
||||||
|
console.error(`[EmailImport] Error checking emails for user ${userId}:`, error);
|
||||||
|
});
|
||||||
|
}, frequencyMs);
|
||||||
|
|
||||||
|
activeIntervals.set(userId, interval);
|
||||||
|
console.log(`[EmailImport] Email import service started for user ${userId}`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[EmailImport] Error starting email import service for user ${userId}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop email import service for a user
|
||||||
|
*/
|
||||||
|
export function stopEmailImportService(userId: number): void {
|
||||||
|
const interval = activeIntervals.get(userId);
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
activeIntervals.delete(userId);
|
||||||
|
console.log(`[EmailImport] Email import service stopped for user ${userId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if email import service is running for a user
|
||||||
|
*/
|
||||||
|
export function isEmailImportServiceRunning(userId: number): boolean {
|
||||||
|
return activeIntervals.has(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all email import services
|
||||||
|
*/
|
||||||
|
export function stopAllEmailImportServices(): void {
|
||||||
|
activeIntervals.forEach((interval, userId) => {
|
||||||
|
clearInterval(interval);
|
||||||
|
console.log(`[EmailImport] Stopped email import service for user ${userId}`);
|
||||||
|
});
|
||||||
|
activeIntervals.clear();
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generat
|
|||||||
import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor";
|
import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor";
|
||||||
import { localStoragePut, generateStorageKey } from "./localStorage";
|
import { localStoragePut, generateStorageKey } from "./localStorage";
|
||||||
import { testSftpConnection, exportInvoiceToSftp, getUserSftpConfig } from "./sftpExport";
|
import { testSftpConnection, exportInvoiceToSftp, getUserSftpConfig } from "./sftpExport";
|
||||||
|
import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning } from "./emailImportService";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
// Admin-only procedure
|
// Admin-only procedure
|
||||||
@@ -575,6 +576,24 @@ export const appRouter = router({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// ============= EMAIL IMPORT SERVICE ROUTES =============
|
||||||
|
emailImportService: router({
|
||||||
|
start: protectedProcedure.mutation(async ({ ctx }) => {
|
||||||
|
const started = await startEmailImportService(ctx.user.id);
|
||||||
|
return { success: started };
|
||||||
|
}),
|
||||||
|
|
||||||
|
stop: protectedProcedure.mutation(async ({ ctx }) => {
|
||||||
|
stopEmailImportService(ctx.user.id);
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
|
|
||||||
|
status: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
const isRunning = isEmailImportServiceRunning(ctx.user.id);
|
||||||
|
return { isRunning };
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
// ============= IMPORT SETTINGS ROUTES =============
|
// ============= IMPORT SETTINGS ROUTES =============
|
||||||
importSettings: router({
|
importSettings: router({
|
||||||
get: protectedProcedure.query(async ({ ctx }) => {
|
get: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
|||||||
11
todo.md
11
todo.md
@@ -148,3 +148,14 @@
|
|||||||
|
|
||||||
## Correction page Paramètres de réception
|
## Correction page Paramètres de réception
|
||||||
- [x] Ajouter DashboardLayout à la page ImportSettings pour afficher le menu latéral
|
- [x] Ajouter DashboardLayout à la page ImportSettings pour afficher le menu latéral
|
||||||
|
|
||||||
|
## Import automatique par email
|
||||||
|
- [x] Installer les dépendances npm pour IMAP (imap et mailparser)
|
||||||
|
- [x] Créer le service emailImportService.ts pour se connecter à IMAP
|
||||||
|
- [x] Implémenter la détection des nouveaux emails non lus avec pièces jointes PDF
|
||||||
|
- [x] Implémenter le téléchargement et traitement des pièces jointes
|
||||||
|
- [x] Implémenter le marquage des emails comme lus après traitement
|
||||||
|
- [x] Créer un scheduler pour exécuter le service selon la fréquence configurée
|
||||||
|
- [x] Ajouter des routes tRPC pour démarrer/arrêter le service
|
||||||
|
- [x] Ajouter des boutons de contrôle du service dans la page Paramètres de réception
|
||||||
|
- [ ] Tester l'import automatique par email avec un vrai compte
|
||||||
|
|||||||
Reference in New Issue
Block a user