SSkilltecabyclaudinhocode
Enviar skill
← Voltar para o catálogo

mailoutofspam

Design e Frontend

Use when emails from a domain are landing in the Gmail/Outlook/Yahoo spam folder and need to recover deliverability — performs DNS auth audit (SPF/DKIM/DMARC), fixes mis-configurations, builds RFC 8058 one-click unsubscribe, adds bounce-parsing + suppression list, cleans the template against spam triggers, throttles per-provider, and verifies via raw-header inspection. Triggered by "mails are in s

1estrelas
Ver no GitHub ↗Autor: pouyA-pngLicença: MIT

mailoutofspam

End-to-end protocol to diagnose and repair email deliverability for a domain whose outbound mail is landing in spam folders. Designed for cold-outreach / commercial-mail senders that have proper authentication on paper but are still flagged by Gmail/Outlook/Yahoo.

This skill is diagnostic-first: it never assumes DNS is broken before checking. In >50% of real cases, SPF/DKIM/DMARC are already correct and the cause is reputation/headers/content. Skip the DNS rewrite if the audit shows pass.


When to invoke

  • A user reports their cold-mails or transactional mails are going to spam
  • After Gmail/Yahoo's Feb 2024 sender requirements rolled out (List-Unsubscribe-Post=One-Click became mandatory for >5000/day senders)
  • DMARC aggregate (rua) reports show pass-rate dropping
  • Mail-tester.com score is below 8/10
  • A bounce-rate spike on a previously-stable mailbox

Hard rules (do not violate)

  1. Pause outbound workflows BEFORE making changes. Do not edit a live-sending pipeline.
  2. Preserve all logs and current configuration — snapshot before mutation.
  3. No spam-evasion tactics — no domain rotation, no fake Re:/Fwd:, no hidden unsubscribe.
  4. Do not move DMARC to p=quarantine or p=reject until SPF/DKIM/DMARC pass consistently. If you find the domain already at quarantine but no aggregate reports verify pass, drop to p=none first, verify, ramp back.
  5. No domain rotation to bypass reputation — fix the existing reputation.

Step 1 — Pause sending

Disable the outbound send workflows. Keep reply-handler / bounce-listener active.

For n8n:

curl -X POST "https://<n8n-host>/api/v1/workflows/<wid>/deactivate" \
  -H "X-N8N-API-KEY: $N8N_API_KEY"

Snapshot all workflow JSON to snapshot/.


Step 2 — DNS authentication audit

Use DNS-over-HTTPS so it works without dig:

import urllib.request, json
def doh(name, rtype):
    url = f"https://1.1.1.1/dns-query?name={name}&type={rtype}"
    r = urllib.request.Request(url, headers={"accept":"application/dns-json"})
    d = json.loads(urllib.request.urlopen(r, timeout=10).read())
    return [a.get("data","").strip('"') for a in d.get("Answer",[])]

DOMAIN = "factbinger-websites.com"
print("A:    ", doh(DOMAIN,"A"))
print("MX:   ", doh(DOMAIN,"MX"))
print("SPF:  ", [t for t in doh(DOMAIN,"TXT") if t.startswith("v=spf1")])
print("DMARC:", doh(f"_dmarc.{DOMAIN}","TXT"))
for sel in ("hostingermail1","default","selector1","google","s1","mail","key1"):
    r = doh(f"{sel}._domainkey.{DOMAIN}","TXT")
    if r and not r[0].startswith("ERR"):
        print(f"DKIM {sel}: {r[0][:120]}…")

Document findings in snapshot/dns_audit.md. Compare against a working sister-domain on the same provider if you have one — if both are identical, DNS is not the problem.


Step 3 — Fix SPF/DKIM/DMARC (only if needed)

  • Exactly one SPF record. Multiple SPF TXTs at the apex break verification.
  • DKIM selector exists — provider-specific (Hostinger uses hostingermail1, Google uses google, etc.). Do not guess; check provider docs.
  • DMARC: start at p=none, full reporting:
    v=DMARC1; p=none; sp=none; pct=100; rua=mailto:dmarc@<domain>; ruf=mailto:dmarc@<domain>; aspf=r; adkim=r; fo=1;
    
    • fo=1 → forensic on any failure (not just full)
    • sp=none → subdomain inheritance
    • The mailbox in rua=mailto: MUST exist. If dmarc@ doesn't exist, reports vanish — repoint to a verified mailbox like contact@ or create the dmarc@ inbox first.
  • Ramp to p=quarantine only after 5–7 days of aggregate reports showing 100% pass-rate.

If using Hostinger, the API rejects merge-mode TXT-record updates — duplicate records will be created. Always:

  1. DELETE the existing TXT name
  2. PUT the new single record

Step 4 — Suppression infrastructure

Five reasons per spec: unsubscribed, hard_bounce, complaint, invalid_email, manual_block.

CREATE TYPE suppression_reason AS ENUM (
  'unsubscribed','hard_bounce','complaint','invalid_email','manual_block'
);
ALTER TABLE suppressions
  ADD COLUMN IF NOT EXISTS reason suppression_reason,
  ADD COLUMN IF NOT EXISTS source TEXT,
  ADD COLUMN IF NOT EXISTS smtp_code TEXT,
  ADD COLUMN IF NOT EXISTS notes TEXT,
  ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
  ADD COLUMN IF NOT EXISTS email_lower TEXT GENERATED ALWAYS AS (LOWER(email)) STORED;
CREATE UNIQUE INDEX IF NOT EXISTS suppressions_idx
  ON suppressions (email_lower, COALESCE(flow,''));

Idempotent suppression check before every send via LOWER(l.email) NOT IN (SELECT email_lower FROM suppressions ...).

Full SQL migration in examples/01_suppression_table.sql.


Step 5 — RFC 8058 one-click unsubscribe

Build an HTTPS endpoint that accepts BOTH:

  • GET /unsubscribe?t=<token> — for the link in the email body (browser visit)
  • POST /unsubscribe with form body t=<token>&List-Unsubscribe=One-Click — what Gmail/Yahoo's mail-clients call automatically when the user clicks the inbox-level unsub button

Token strategy: random URL-safe 32-char string (NOT HMAC if your sandbox blocks crypto). Store in unsub_tokens(token, email, flow, used_at, ip, user_agent) — generated at email-render time, looked up on hit.

Headers added to every commercial email before DKIM signing:

List-Unsubscribe: <https://example.com/unsubscribe?t=TOKEN>, <mailto:contact@example.com?subject=unsubscribe>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Precedence: bulk
Auto-Submitted: auto-generated

Verify the headers ARE in the DKIM h= list — otherwise mail-clients will not trust them for one-click action.

n8n implementation: see examples/n8n_unsubscribe_workflow.json. Two parallel webhook nodes (one for GET, one for POST) merging into the same Code → Postgres → Respond chain. DO NOT use multipleMethods on a single webhook — it splits outputs and the downstream Code node only receives one branch.


Step 6 — Bounce parsing + auto-suppress

DSN (Delivery Status Notification) detection in the reply-handler:

const fromIsDaemon = /(mailer-daemon|postmaster|do[-_]?not[-_]?reply|noreply|bounces?@)/i.test(from);
const subjIsBounce = /(undelivered|delivery (failed|status|notification)|returned|failure notice|mail.*failed|bounced?|nicht zustellbar|unzustellbar)/i.test(subject);
const bodyHasSmtp5xx = /\b5\d{2}\s/.test(body) || /Status:\s*5\.\d/.test(body);
const is_bounce = (fromIsDaemon && (subjIsBounce || bodyHasSmtp5xx)) || (subjIsBounce && bodyHasSmtp5xx);

// Extract failed recipient
const xfail = body.match(/X-Failed-Recipients:\s*([^\r\n,]+)/i);
const finalrcpt = body.match(/Final-Recipient:\s*(?:rfc822;)?\s*([^\r\n;,\s]+)/i);
const angled = body.match(/<([^@<>\s]+@[^@<>\s]+)>/);
const bounced_email = (xfail||finalrcpt||angled||[])[1] || '';

// SMTP code
const codeMatch = body.match(/\b(5\d{2})\s+(\d\.\d\.\d)?\s*([^\r\n]{0,120})/);

On detection: insert into suppressions with reason='hard_bounce', source='smtp-bounce', smtp_code=…, idempotent via the unique index.


Step 7 — Template cleanup

  • Plaintext-first, optional minimal HTML mirror (multipart/alternative)
  • ONE clear CTA (the reply, plus visible unsubscribe link — no other links)
  • No attachments
  • Sender identity, business name, postal address, imprint URL all visible in footer
  • Defense-in-depth strip: collapse !{2,}, lowercase GRATIS|KOSTENLOS|JETZT|EXKLUSIV|RIESEN, drop $$$/€€€

Salutation: Sehr geehrte/r <Lastname> for B2B, <Firma>-Team as fallback. Avoid Hallo for cold-outreach.


Step 8 — Throttle

  • Disable any burst-sending node
  • Per-provider hourly cap via Postgres helper:
    CREATE FUNCTION fn_under_provider_cap(_d TEXT, _cap INT, _w INTERVAL DEFAULT '1 hour') RETURNS BOOLEAN
      LANGUAGE SQL STABLE AS $$
      SELECT COUNT(*) < _cap FROM provider_rate_log
        WHERE recipient_domain = LOWER(_d) AND sent_at >= NOW() - _w
    

Como adicionar

/plugin marketplace add pouyA-png/mailoutofspam

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Comentários · Nenhum comentário

Entre para comentar. Entrar

  • Ainda não há comentários. Seja o primeiro.