feat(email): incoming mail (IMAP) intake - backend + standalone flag

Adds a second mail config (incoming/IMAP) alongside the outgoing SMTP one, a
1-minute poller, and a received-emails log. Standalone `incomingMail` feature
flag (default off).

- deps: imapflow + mailparser (receive-side; picpeak only had nodemailer).
- migration 128: email_configs gains imap_* columns (same shape as smtp_*);
  seed incomingMail flag; new received_emails audit table.
- emailIntakeService: polls the mailbox every 60s when the flag is on AND a
  mailbox is configured (no-op otherwise); parses each unseen message
  (mailparser flattens forwarded/nested attachments), drops PDF/JPEG/PNG into
  the incoming-invoices inbox (inbound_documents, source='email'), logs each
  message in received_emails (dedupe by message-id; duplicate attachments
  caught by the existing SHA-256 guard), marks it \Seen.
- adminEmail: GET/POST /incoming-config (mirrors SMTP config, masks imap_pass,
  SSRF host guard) + GET /received (paginated log).
- server.js starts the poller at boot.

Verified: node -c, require-graph, migration-128 harness (imap columns, flag,
received_emails). Frontend (IMAP block under SMTP + Received tab + flag card)
follows.
This commit is contained in:
Luca
2026-06-11 15:54:43 +02:00
parent 81af4453e7
commit 5645c304ab
7 changed files with 666 additions and 13 deletions
+10 -1
View File
@@ -836,7 +836,16 @@ async function startServer() {
logger.warn('Email template self-heal failed at boot:', err.message);
}
startEmailQueueProcessor();
// Start incoming-mail (IMAP) poller — no-ops each minute unless the
// `incomingMail` flag is on and a mailbox is configured (migration 128).
try {
const { startIncomingMailPoller } = require('./src/services/emailIntakeService');
startIncomingMailPoller();
} catch (err) {
logger.warn('Incoming-mail poller failed to start:', err.message);
}
// Start webhook delivery worker (#327)
const { startWebhookDeliveryWorker } = require('./src/services/webhookDeliveryWorker');
startWebhookDeliveryWorker();