diff --git a/client/src/pages/ImportSettings.tsx b/client/src/pages/ImportSettings.tsx
index 2d34adc..9014fb6 100644
--- a/client/src/pages/ImportSettings.tsx
+++ b/client/src/pages/ImportSettings.tsx
@@ -6,12 +6,15 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
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";
export default function ImportSettings() {
const { data: settings, isLoading } = trpc.importSettings.get.useQuery();
+ const { data: serviceStatus } = trpc.emailImportService.status.useQuery();
const updateMutation = trpc.importSettings.update.useMutation();
+ const startServiceMutation = trpc.emailImportService.start.useMutation();
+ const stopServiceMutation = trpc.emailImportService.stop.useMutation();
// Manual import
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)
+
+ {/* Service Control Buttons */}
+
+ {serviceStatus?.isRunning ? (
+
+ ) : (
+
+ )}
+ {serviceStatus?.isRunning && (
+
+
+ Service actif
+
+ )}
+
>
)}
diff --git a/package.json b/package.json
index 4ce9f54..2b76337 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,9 @@
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
"@types/bcrypt": "^6.0.0",
+ "@types/imap": "^0.8.43",
"@types/jsonwebtoken": "^9.0.10",
+ "@types/mailparser": "^3.4.6",
"@types/ssh2-sftp-client": "^9.0.6",
"axios": "^1.12.0",
"bcrypt": "^6.0.0",
@@ -62,10 +64,12 @@
"embla-carousel-react": "^8.6.0",
"express": "^4.21.2",
"framer-motion": "^12.23.22",
+ "imap": "^0.8.19",
"input-otp": "^1.4.2",
"jose": "6.1.0",
"jsonwebtoken": "^9.0.3",
"lucide-react": "^0.453.0",
+ "mailparser": "^3.9.3",
"mysql2": "^3.15.0",
"nanoid": "^5.1.5",
"next-themes": "^0.4.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4c7a483..9f56c35 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -121,9 +121,15 @@ importers:
'@types/bcrypt':
specifier: ^6.0.0
version: 6.0.0
+ '@types/imap':
+ specifier: ^0.8.43
+ version: 0.8.43
'@types/jsonwebtoken':
specifier: ^9.0.10
version: 9.0.10
+ '@types/mailparser':
+ specifier: ^3.4.6
+ version: 3.4.6
'@types/ssh2-sftp-client':
specifier: ^9.0.6
version: 9.0.6
@@ -163,6 +169,9 @@ importers:
framer-motion:
specifier: ^12.23.22
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:
specifier: ^1.4.2
version: 1.4.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
@@ -175,6 +184,9 @@ importers:
lucide-react:
specifier: ^0.453.0
version: 0.453.0(react@19.2.1)
+ mailparser:
+ specifier: ^3.9.3
+ version: 3.9.3
mysql2:
specifier: ^3.15.0
version: 3.15.1
@@ -1894,6 +1906,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@selderee/plugin-htmlparser2@0.11.0':
+ resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
+
'@shikijs/core@3.14.0':
resolution: {integrity: sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==}
@@ -2399,12 +2414,18 @@ packages:
'@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
+ '@types/imap@0.8.43':
+ resolution: {integrity: sha512-POPoqrDax9mxM2N4ITZYCWaFtg1ORVfzJe4S7xwSh9aHawdEb7FwWTJYiAhzIvWp7DM+6BajnzYOwZ1BUrqtow==}
+
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
'@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
+ '@types/mailparser@3.4.6':
+ resolution: {integrity: sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==}
+
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -2496,6 +2517,9 @@ packages:
'@vitest/utils@2.1.9':
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:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
@@ -2705,6 +2729,9 @@ packages:
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
engines: {node: '>=12.13'}
+ core-util-is@1.0.3:
+ resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+
cose-base@1.0.3:
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
@@ -2920,6 +2947,10 @@ packages:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
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:
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
@@ -2956,9 +2987,22 @@ packages:
dom-helpers@5.2.1:
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:
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:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'}
@@ -3093,10 +3137,18 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
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:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
@@ -3339,12 +3391,23 @@ packages:
hastscript@9.0.1:
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:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+ htmlparser2@8.0.2:
+ resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
+
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
@@ -3361,6 +3424,14 @@ packages:
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
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:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@@ -3407,6 +3478,9 @@ packages:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'}
+ isarray@0.0.1:
+ resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
+
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
@@ -3457,6 +3531,18 @@ packages:
layout-base@2.0.1:
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:
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
engines: {node: '>= 12.0.0'}
@@ -3521,6 +3607,9 @@ packages:
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
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:
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
engines: {node: '>=14'}
@@ -3589,6 +3678,9 @@ packages:
magic-string@0.30.19:
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:
resolution: {integrity: sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==}
@@ -3848,6 +3940,10 @@ packages:
node-releases@2.0.23:
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:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'}
@@ -3882,6 +3978,9 @@ packages:
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+ parseley@0.12.1:
+ resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
+
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -3909,6 +4008,9 @@ packages:
resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==}
engines: {node: '>=20.16.0 || >=22.3.0'}
+ peberminta@0.9.0:
+ resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -3965,6 +4067,10 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+ punycode.js@2.3.1:
+ resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+ engines: {node: '>=6'}
+
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
@@ -4075,6 +4181,9 @@ packages:
resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==}
engines: {node: '>=0.10.0'}
+ readable-stream@1.1.14:
+ resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==}
+
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@@ -4152,6 +4261,13 @@ packages:
scheduler@0.27.0:
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:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
@@ -4248,6 +4364,9 @@ packages:
peerDependencies:
react: ^18.0.0 || ^19.0.0
+ string_decoder@0.10.31:
+ resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
+
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@@ -4317,6 +4436,10 @@ packages:
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
engines: {node: '>=14.0.0'}
+ tlds@1.261.0:
+ resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==}
+ hasBin: true
+
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
@@ -4360,6 +4483,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ uc.micro@2.1.0:
+ resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
@@ -4428,6 +4554,9 @@ packages:
peerDependencies:
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:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -6348,6 +6477,11 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.52.4':
optional: true
+ '@selderee/plugin-htmlparser2@0.11.0':
+ dependencies:
+ domhandler: 5.0.3
+ selderee: 0.11.0
+
'@shikijs/core@3.14.0':
dependencies:
'@shikijs/types': 3.14.0
@@ -7006,6 +7140,10 @@ snapshots:
'@types/http-errors@2.0.5': {}
+ '@types/imap@0.8.43':
+ dependencies:
+ '@types/node': 24.7.0
+
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
@@ -7013,6 +7151,11 @@ snapshots:
'@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':
dependencies:
'@types/unist': 3.0.3
@@ -7125,6 +7268,12 @@ snapshots:
loupe: 3.2.1
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:
dependencies:
mime-types: 2.1.35
@@ -7333,6 +7482,8 @@ snapshots:
dependencies:
is-what: 4.1.16
+ core-util-is@1.0.3: {}
+
cose-base@1.0.3:
dependencies:
layout-base: 1.0.2
@@ -7559,6 +7710,8 @@ snapshots:
deep-eql@5.0.2: {}
+ deepmerge@4.3.1: {}
+
delaunator@5.0.1:
dependencies:
robust-predicates: 3.0.2
@@ -7586,10 +7739,28 @@ snapshots:
'@babel/runtime': 7.28.4
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:
optionalDependencies:
'@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: {}
drizzle-kit@0.31.5:
@@ -7635,11 +7806,15 @@ snapshots:
encodeurl@2.0.0: {}
+ encoding-japanese@2.2.0: {}
+
enhanced-resolve@5.18.3:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.0
+ entities@4.5.0: {}
+
entities@6.0.1: {}
es-define-property@1.0.1: {}
@@ -8028,10 +8203,27 @@ snapshots:
property-information: 7.1.0
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-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:
dependencies:
depd: 2.0.0
@@ -8052,6 +8244,15 @@ snapshots:
dependencies:
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: {}
inline-style-parser@0.2.4: {}
@@ -8084,6 +8285,8 @@ snapshots:
is-what@4.1.16: {}
+ isarray@0.0.1: {}
+
jiti@2.6.1: {}
jose@6.1.0: {}
@@ -8138,6 +8341,19 @@ snapshots:
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:
optional: true
@@ -8183,6 +8399,10 @@ snapshots:
lightningcss-win32-arm64-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:
dependencies:
mlly: 1.8.0
@@ -8237,6 +8457,19 @@ snapshots:
dependencies:
'@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-event-props@2.0.0: {}
@@ -8719,6 +8952,8 @@ snapshots:
node-releases@2.0.23: {}
+ nodemailer@7.0.13: {}
+
normalize-range@0.1.2: {}
object-assign@4.1.1: {}
@@ -8755,6 +8990,11 @@ snapshots:
dependencies:
entities: 6.0.1
+ parseley@0.12.1:
+ dependencies:
+ leac: 0.6.0
+ peberminta: 0.9.0
+
parseurl@1.3.3: {}
path-data-parser@0.1.0: {}
@@ -8778,6 +9018,8 @@ snapshots:
optionalDependencies:
'@napi-rs/canvas': 0.1.88
+ peberminta@0.9.0: {}
+
picocolors@1.1.1: {}
picomatch@4.0.3: {}
@@ -8835,6 +9077,8 @@ snapshots:
proxy-from-env@1.1.0: {}
+ punycode.js@2.3.1: {}
+
qs@6.13.0:
dependencies:
side-channel: 1.1.0
@@ -8956,6 +9200,13 @@ snapshots:
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:
dependencies:
inherits: 2.0.4
@@ -9099,6 +9350,12 @@ snapshots:
scheduler@0.27.0: {}
+ selderee@0.11.0:
+ dependencies:
+ parseley: 0.12.1
+
+ semver@5.3.0: {}
+
semver@6.3.1: {}
semver@7.7.3: {}
@@ -9236,6 +9493,8 @@ snapshots:
- '@types/react'
- supports-color
+ string_decoder@0.10.31: {}
+
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
@@ -9298,6 +9557,8 @@ snapshots:
tinyspy@3.0.2: {}
+ tlds@1.261.0: {}
+
toidentifier@1.0.1: {}
trim-lines@3.0.1: {}
@@ -9330,6 +9591,8 @@ snapshots:
typescript@5.9.3: {}
+ uc.micro@2.1.0: {}
+
ufo@1.6.1: {}
undici-types@5.26.5: {}
@@ -9406,6 +9669,10 @@ snapshots:
dependencies:
react: 19.2.1
+ utf7@1.0.2:
+ dependencies:
+ semver: 5.3.0
+
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}
diff --git a/server/emailImportService.ts b/server/emailImportService.ts
new file mode 100644
index 0000000..3f78b93
--- /dev/null
+++ b/server/emailImportService.ts
@@ -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();
+
+/**
+ * Process a single email attachment (PDF)
+ * Replicates the same logic as manual upload
+ */
+async function processEmailAttachment(
+ userId: number,
+ attachment: Attachment,
+ emailSubject: string
+): Promise {
+ 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 {
+ 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 {
+ 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();
+}
diff --git a/server/routers.ts b/server/routers.ts
index ca206b9..9951cd5 100644
--- a/server/routers.ts
+++ b/server/routers.ts
@@ -33,6 +33,7 @@ import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generat
import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor";
import { localStoragePut, generateStorageKey } from "./localStorage";
import { testSftpConnection, exportInvoiceToSftp, getUserSftpConfig } from "./sftpExport";
+import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning } from "./emailImportService";
import { TRPCError } from "@trpc/server";
// 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 =============
importSettings: router({
get: protectedProcedure.query(async ({ ctx }) => {
diff --git a/todo.md b/todo.md
index 6f34866..656fb98 100644
--- a/todo.md
+++ b/todo.md
@@ -148,3 +148,14 @@
## Correction page Paramètres de réception
- [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