diff --git a/src/assets/screenshots/email/email-quarantine-actions.png b/src/assets/screenshots/email/email-quarantine-actions.png new file mode 100644 index 0000000..ddf3d54 Binary files /dev/null and b/src/assets/screenshots/email/email-quarantine-actions.png differ diff --git a/src/assets/screenshots/email/email-quarantine-list.png b/src/assets/screenshots/email/email-quarantine-list.png new file mode 100644 index 0000000..b11e3fd Binary files /dev/null and b/src/assets/screenshots/email/email-quarantine-list.png differ diff --git a/src/assets/screenshots/email/email-quarantine-login.png b/src/assets/screenshots/email/email-quarantine-login.png new file mode 100644 index 0000000..b8e9c4a Binary files /dev/null and b/src/assets/screenshots/email/email-quarantine-login.png differ diff --git a/src/assets/screenshots/email/email-quarantine-request.png b/src/assets/screenshots/email/email-quarantine-request.png new file mode 100644 index 0000000..06d5fb9 Binary files /dev/null and b/src/assets/screenshots/email/email-quarantine-request.png differ diff --git a/src/content/docs/email/release-quarantined-email.mdx b/src/content/docs/email/release-quarantined-email.mdx index 11a1f90..19f7abe 100644 --- a/src/content/docs/email/release-quarantined-email.mdx +++ b/src/content/docs/email/release-quarantined-email.mdx @@ -8,50 +8,46 @@ sidebar: import { Steps, Aside } from '@astrojs/starlight/components'; import Support from '~/content/partials/support-link.mdx'; -Mail our filters flag as likely spam isn't delivered to your inbox — it's held in **quarantine** instead, where you can review it, release anything that's actually wanted, and always allow a sender in future. - - +Mail our filters flag as likely spam isn't delivered to your inbox — it's held in **quarantine** instead, where you can review it, release anything that's actually wanted, and always allow a sender in future. You don't have to wait for the daily notice to do this — see below. ## Check your quarantine +You'll normally get a **daily summary email** listing anything held for you — but the steps below work any time, not just after that email arrives. There's no separate password to remember; a link emailed to your own address is what signs you in. + -1. Go to your mail filtering portal at `https://pmg01.cloud-hosting.io:8006/quarantine`. +1. Go to your mail filtering portal at `https://pmg01.cloud-hosting.io:8006/quarantine`. Choose **Request Quarantine Link**. -2. Enter the email address you want to check, and choose to receive a sign-in link. + ![The mail filtering portal's login screen, with Request Quarantine Link next to Login](~/assets/screenshots/email/email-quarantine-login.png) -3. Open the email that arrives and follow the link — it signs you in without a separate password. +2. Enter the email address you want to check. -4. Review what's listed. For anything you want, choose **Deliver** to send it to your inbox as normal. + ![The Request Quarantine Link dialog with an email address entered](~/assets/screenshots/email/email-quarantine-request.png) + +3. Open the email that arrives — usually within moments — and follow the link inside it. + +4. Review what's listed for your address. + + ![The Spam Quarantine list showing a held message with its sender, subject, and spam score](~/assets/screenshots/email/email-quarantine-list.png) + +5. Select a message to see its content and the Whitelist / Blacklist / Deliver / Delete options, then choose **Deliver** to send it to your inbox as normal. + + ![A held message selected, showing its content and the Whitelist, Blacklist, Deliver, and Delete buttons](~/assets/screenshots/email/email-quarantine-actions.png) + + ## Always allow a sender -If mail from someone keeps getting caught and you know it's legitimate, allow their address (or their whole domain) from the same screen — choose **Whitelist** on a held message, or add the address directly from the quarantine portal. Future mail from that sender skips spam filtering for **your address specifically** — it doesn't change anything for the rest of your organization. +If mail from someone keeps getting caught and you know it's legitimate, allow their address (or their whole domain) from the same screen — choose **Whitelist** on a held message (shown in the screenshot above), or add the address directly from the quarantine portal. Future mail from that sender skips spam filtering for **your address specifically** — it doesn't change anything for the rest of your organization. -## Get in without waiting - -You'll normally get a **daily summary email** listing anything held for you, with links to review and release it from there. But if something time-sensitive is stuck in quarantine, you don't need to wait for that email: - - - -1. Go to `https://pmg01.cloud-hosting.io:8006/quarantine` any time. - -2. Enter your email address and request a sign-in link — a fresh one arrives within moments. - -3. Follow the link and release the message immediately. - - - -This works any time of day and isn't limited to once daily — request a new link as often as you need one. - ## Common problems **The sign-in link email hasn't arrived.** Check your spam folder — slightly ironic for a spam-filtering tool, but it happens. If it's still missing after a few minutes, contact us. diff --git a/tools/screenshots/README.md b/tools/screenshots/README.md index fee78d5..76e682a 100644 --- a/tools/screenshots/README.md +++ b/tools/screenshots/README.md @@ -63,6 +63,7 @@ npx tsx tools/screenshots/capture-dns.ts | `capture-site-builder.ts` | Site Builder editor states | `WHP_USER` | | `capture-dns.ts` | Domains & DNS list, Add Domain modal, records editor, bulk toolbar | `WHP_USER` | | `capture-email.ts` | Email page "Mail Client Setup" section (autodiscovery DNS records) | `WHP_USER` | +| `capture-quarantine.ts` | PMG spam quarantine portal (login, request-link dialog, held-mail list, message actions) — **not a WHP page**, targets the mail filtering gateway directly | `PMG_BASE` + `PMG_TICKET_URL` (a real quarantine ticket link, e.g. from a test mailbox's "Request Quarantine Link" email) instead of WHP_USER/PASS | Each script carries its own `redact()` (text-node + input-value swaps) so fleet hostnames, IPs, and customer data become neutral placeholders while brand/demo domains stay visible. Copy the closest existing script when adding a new section — match its viewport (1440×900), `deviceScaleFactor: 2`, and **read-only** discipline (open modals and tick boxes for the shot, but never save/delete/submit). diff --git a/tools/screenshots/capture-quarantine.ts b/tools/screenshots/capture-quarantine.ts new file mode 100644 index 0000000..c8f7838 --- /dev/null +++ b/tools/screenshots/capture-quarantine.ts @@ -0,0 +1,118 @@ +/** + * PMG quarantine capture — the customer self-service spam quarantine portal. + * + * Captures: + * - email-quarantine-login.png Login screen showing the "Request Quarantine + * Link" button next to Login. + * - email-quarantine-request.png The "Request Quarantine Link" dialog with an + * email address entered. + * - email-quarantine-list.png The authenticated quarantine grid showing held + * mail, redacted to generic example content. + * - email-quarantine-actions.png A selected message with the Whitelist / + * Blacklist / Deliver / Delete toolbar visible. + * + * Viewport-only (1440x900, deviceScaleFactor 2), redacted: the real server + * hostname, the real test mailbox address, and the real (internal-test-looking) + * sender/subject/body text are all swapped for generic customer-facing examples. + * + * Read-only: never actually delivers/whitelists/blacklists/deletes anything. + */ +import { chromium, type Page } from 'playwright'; +import { mkdir } from 'node:fs/promises'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const OUT_DIR = resolve(__dirname, '../../src/assets/screenshots/email'); + +const BASE = process.env.PMG_BASE || 'https://pmg01.cloud-hosting.io:8006'; +const TICKET_URL = process.env.PMG_TICKET_URL; // full quarantine?ticket=... URL + +/** Swap real fleet/test identifiers for believable generic customer-facing text. */ +async function redact(page: Page) { + await page.evaluate(() => { + const swaps: [RegExp, string][] = [ + [/pmg01\.cloud-hosting\.io/gi, 'mail.yourdomain.com'], + [/claude@darksideofperfection\.com/gi, 'you@yourdomain.com'], + [/pmg-pilot-tester@anthropic\.com/gi, 'prizes@totally-legit-sweepstakes.example'], + [/SPAM: PMG pilot - spam detection test \(GTUBE\) \(retry\)/g, "SPAM: You've WON a prize!!!"], + [/SPAM: PMG pilot - clean delivery test \(retry\)/g, 'SPAM: Limited time offer - act now'], + [/This is a clean end-to-end delivery test, retried after greylist window\./g, + 'Congratulations! You have been selected to receive an exclusive reward. Click below to claim yours before this offer expires.'], + [/GTUBE retry test\./g, "You've been selected for an exclusive reward!"], + [/XJS\*C4JDBQADN1\.NSBN3\*2IDNEN\*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL\*C\.34X/g, + 'Click here to claim your prize before this limited-time offer expires: http://totally-legit-sweepstakes.example/claim'], + [/\b1010\b/g, '8.4'], + ]; + const docs: Document[] = [document]; + for (const f of Array.from(document.querySelectorAll('iframe'))) { + try { + const doc = (f as HTMLIFrameElement).contentDocument; + if (doc) docs.push(doc); + } catch { /* cross-origin, skip */ } + } + for (const root of docs) { + const walker = root.createTreeWalker(root.body, NodeFilter.SHOW_TEXT); + const nodes: Text[] = []; + let n: Node | null = walker.nextNode(); + while (n) { nodes.push(n as Text); n = walker.nextNode(); } + for (const node of nodes) { + let v = node.nodeValue ?? ''; + for (const [re, rep] of swaps) v = v.replace(re, rep); + if (v !== node.nodeValue) node.nodeValue = v; + } + } + }); +} + +async function main() { + if (!TICKET_URL) throw new Error('missing env: PMG_TICKET_URL'); + await mkdir(OUT_DIR, { recursive: true }); + const browser = await chromium.launch({ headless: true }); + const ctx = await browser.newContext({ + ignoreHTTPSErrors: true, + viewport: { width: 1440, height: 900 }, + deviceScaleFactor: 2, + }); + const page = await ctx.newPage(); + try { + // --- Shot 1: login screen, showing the Request Quarantine Link button --- + await page.goto(`${BASE}/quarantine`, { waitUntil: 'networkidle' }); + await page.waitForSelector('text=Request Quarantine Link', { state: 'visible' }); + await page.waitForTimeout(300); + await redact(page); + let p = resolve(OUT_DIR, 'email-quarantine-login.png'); + await page.locator('.x-window').first().screenshot({ path: p }); + console.log(`captured email-quarantine-login -> ${p}`); + + // --- Shot 2: the Request Quarantine Link dialog, filled in --- + await page.getByRole('button', { name: 'Request Quarantine Link' }).click(); + await page.getByRole('textbox', { name: 'Your E-Mail:' }).fill('you@yourdomain.com'); + await page.waitForTimeout(200); + p = resolve(OUT_DIR, 'email-quarantine-request.png'); + await page.locator('.x-window').last().screenshot({ path: p }); + console.log(`captured email-quarantine-request -> ${p}`); + + // --- Shot 3 & 4: authenticated quarantine view, via the real ticket link --- + await page.goto(TICKET_URL, { waitUntil: 'networkidle' }); + await page.waitForSelector('grid[aria-label="Spam Quarantine"], .x-grid', { state: 'visible' }).catch(() => {}); + await page.waitForTimeout(500); + await redact(page); + p = resolve(OUT_DIR, 'email-quarantine-list.png'); + await page.screenshot({ path: p }); + console.log(`captured email-quarantine-list -> ${p}`); + + // Select the first row to reveal the action toolbar + message preview. + const firstRow = page.locator('.x-grid-row').first(); + await firstRow.click(); + await page.waitForTimeout(500); + await redact(page); + p = resolve(OUT_DIR, 'email-quarantine-actions.png'); + await page.screenshot({ path: p }); + console.log(`captured email-quarantine-actions -> ${p}`); + } finally { + await browser.close(); + } +} + +main().catch((err) => { console.error(err); process.exit(1); });