diff --git a/mcp_modules/backlinks/src/email-policy.js b/mcp_modules/backlinks/src/email-policy.js new file mode 100644 index 0000000..037e4c5 --- /dev/null +++ b/mcp_modules/backlinks/src/email-policy.js @@ -0,0 +1,12 @@ +/** A mailbox-specific emergency stop shared with other Profullstack senders. */ +export function assertEmailSenderAllowed(...addresses) { + const blocked = new Set( + (process.env.EMAIL_QUEUE_BLOCKED_SENDERS || '') + .split(',') + .map(address => address.trim().toLowerCase()) + .filter(Boolean) + ); + if (addresses.some(address => typeof address === 'string' && blocked.has(address.trim().toLowerCase()))) { + throw new Error('Outbound sending from this mailbox is paused for administrator review.'); + } +} diff --git a/mcp_modules/backlinks/src/service.js b/mcp_modules/backlinks/src/service.js index 8704994..3720503 100644 --- a/mcp_modules/backlinks/src/service.js +++ b/mcp_modules/backlinks/src/service.js @@ -8,6 +8,7 @@ import puppeteer from 'puppeteer'; import nodemailer from 'nodemailer'; import { v4 as uuidv4 } from 'uuid'; import { backlinksDb } from './database.js'; +import { assertEmailSenderAllowed } from './email-policy.js'; import { validateSubmissionData, generateSubmissionContent, @@ -655,6 +656,7 @@ export class BacklinksService { * @returns {Promise} Email result */ async sendMailgunEmail(submission, campaign, emailConfig) { + assertEmailSenderAllowed(emailConfig.from_email || `noreply@${emailConfig.mailgun_domain}`); const formData = new FormData(); formData.append( 'from', @@ -706,6 +708,7 @@ export class BacklinksService { * @returns {Promise} Email result */ async sendSmtpEmail(submission, campaign, emailConfig) { + assertEmailSenderAllowed(emailConfig.smtp_user, emailConfig.from_email); const transporter = nodemailer.createTransporter({ host: emailConfig.smtp_host, port: emailConfig.smtp_port || 587, diff --git a/mcp_modules/backlinks/test/sending.test.js b/mcp_modules/backlinks/test/sending.test.js new file mode 100644 index 0000000..44e2658 --- /dev/null +++ b/mcp_modules/backlinks/test/sending.test.js @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { assertEmailSenderAllowed } from '../src/email-policy.js'; + +test('blocks a configured mailbox before SMTP or Mailgun is called', () => { + const previous = process.env.EMAIL_QUEUE_BLOCKED_SENDERS; + process.env.EMAIL_QUEUE_BLOCKED_SENDERS = 'other@gmail.com, ettinger@gmail.com'; + try { + assert.throws(() => assertEmailSenderAllowed('ettinger@gmail.com'), /paused for administrator review/); + assert.throws(() => assertEmailSenderAllowed('ETTINGER@gmail.com')); + assert.doesNotThrow(() => assertEmailSenderAllowed('business@example.com')); + } finally { + if (previous === undefined) delete process.env.EMAIL_QUEUE_BLOCKED_SENDERS; + else process.env.EMAIL_QUEUE_BLOCKED_SENDERS = previous; + } +}); diff --git a/package.json b/package.json index 641864b..8cba4dc 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "puppeteer-extra": "^3.3.6", "puppeteer-extra-plugin-stealth": "^2.11.2", "stream-transform": "^3.5.1", - "zod": "4.6.5" + "zod": "4.6.7" }, "devDependencies": { "chai": "^4.5.0",