Setting EMAIL_WEBHOOK_URL makes PicPeak stop sending mail itself and POST each composed message as JSON instead, for something downstream (n8n, Make, a self-hosted relay) to deliver. Unset, every SMTP path is unchanged. Settles the four things #1225 left open: - SSRF: the URL goes through the same DNS-resolving check the outbound webhook worker uses, before every send. Private receivers are opt-in. - Transport security: https is required for anything leaving the machine. The HMAC proves who sent the body, not who can read it, and these bodies carry password-reset links and guest recovery codes. The private-network opt-in doubles as the plaintext opt-in. - Authentication: EMAIL_WEBHOOK_SECRET is required and signs the body as X-PicPeak-Signature, the same scheme as gallery webhooks. A URL without a secret leaves the transport OFF and says so once. - Attachments: carried as base64, not dropped. Oversized ones fail and stay queued rather than arriving without the invoice. Configuration is environment-only on purpose: this redirects every outbound message including password resets, so it must not be changeable from a compromised admin session. Three wiring details decide whether it works at all: docker-compose.yml declares an explicit environment block, so the vars had to be forwarded there; a fresh webhook-only install has no email_configs row (migration 001 seeds it only when SMTP_HOST is set), so the From identity falls back to EMAIL_FROM; and processEmailQueue used to return early when SMTP could not initialise, which would have left the queue permanently unprocessed. guestRecoveryService and the admin test-email endpoint were bypassing the transport — the first dereferenced a null transporter, the second told webhook-only admins to go configure SMTP. emailIntakeService deliberately stays on SMTP: it round-trips a specific mailbox's own credentials. Response handling is streamed and read bounded by hand rather than capped via axios: maxContentLength throws while reading, so a receiver that delivered the mail and then echoed a large body would have been recorded as failed and the message sent again. Note: docker-compose.dev.yml is gitignored and local-only, so the equivalent entries there are not part of this change. docker-compose.production.yml needs none — it passes .env through with env_file. Three rounds of external review; 21 transport tests, 61 across the email suites.
This commit is contained in:
+9
-2
@@ -81,6 +81,7 @@ const { startDownloadJobCleanup } = require('./src/services/downloadJobCleanupSe
|
||||
const { startRevealScheduler } = require('./src/services/revealScheduler');
|
||||
const { startInvoiceScheduler } = require('./src/services/invoiceSchedulerService');
|
||||
const { initializeTransporter, startEmailQueueProcessor } = require('./src/services/emailProcessor');
|
||||
const emailWebhookTransport = require('./src/services/emailWebhookTransport');
|
||||
const { startBackupService } = require('./src/services/backupService');
|
||||
const { startScheduledBackups } = require('./src/services/databaseBackup');
|
||||
const backgroundProcessor = require('./src/services/backgroundProcessor');
|
||||
@@ -1069,8 +1070,14 @@ async function startServer() {
|
||||
// flag is OFF (the service short-circuits on empty result sets).
|
||||
startInvoiceScheduler();
|
||||
|
||||
// Initialize email transporter and start queue processor
|
||||
await initializeTransporter();
|
||||
// Initialize email transporter and start queue processor.
|
||||
// Skipped under the webhook transport (#1225): an install that switched to
|
||||
// it may still carry an old, now-unreachable SMTP row, and nodemailer's
|
||||
// verify() would sit on a connection timeout here — delaying boot for a
|
||||
// transport that will never send anything.
|
||||
if (!emailWebhookTransport.isEnabled()) {
|
||||
await initializeTransporter();
|
||||
}
|
||||
// Seed CRM / contract / event-reminder email templates and recover
|
||||
// any queue rows that exhausted retries because their template
|
||||
// didn't exist yet. Runs once per boot via module-level caches in
|
||||
|
||||
Reference in New Issue
Block a user