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