AgentStack
SKILL verified MIT Self-run

Mailoutofspam

skill-pouya-png-mailoutofspam-mailoutofspam · by pouyA-png

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…

No reviews yet
0 installs
2 views
0.0% view→install

Install

$ agentstack add skill-pouya-png-mailoutofspam-mailoutofspam

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Mailoutofspam? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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:///api/v1/workflows//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@; ruf=mailto:dmarc@; 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= — for the link in the email body (browser visit)
  • POST /unsubscribe with form body t=&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: , 
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 for B2B, -Team as fallback. Avoid Hallo for cold-outreach.


Step 8 — Throttle

  • Disable any burst-sending node
  • Per-provider hourly cap via Postgres helper:

``sql CREATE FUNCTION fn_under_provider_cap(_d TEXT, _cap INT, _w INTERVAL DEFAULT '1 hour') RETURNS BOOLEAN LANGUAGE SQL STABLE AS $$ SELECT COUNT(*) = NOW() - _w $$; ``

  • 5/h to gmail/outlook/hotmail/live/yahoo/aol/t-online/gmx/web; 10/h to others
  • Daily cap during reputation recovery: 10/day total (ramp later)
  • Log every SMTP response code in sends.smtp_response_code + smtp_response_text

Step 9 — Verify (the only success criterion)

Send three test mails (one each to Gmail, Outlook, Yahoo). Pull raw headers via IMAP from a self-loopback if you don't have access to the recipient inboxes:

import imaplib, email, re
m = imaplib.IMAP4_SSL("imap.example.com", 993)
m.login("info@example.com", PW)
m.select("INBOX")
typ, data = m.search(None, "ALL")
for mid in data[0].split()[-1:]:
    _, msg_data = m.fetch(mid, "(RFC822)")
    msg = email.message_from_bytes(msg_data[0][1])
    for h in ("From","To","Subject","Authentication-Results","DKIM-Signature",
              "List-Unsubscribe","List-Unsubscribe-Post","Message-ID","Received-SPF"):
        for v in msg.get_all(h, []):
            print(h, ":", re.sub(r"\s+"," ",v).strip()[:300])

Required pass-criteria in Authentication-Results:

  • dkim=pass header.d=
  • spf=pass
  • dmarc=pass

Required headers present:

  • List-Unsubscribe (URL + mailto)
  • List-Unsubscribe-Post: List-Unsubscribe=One-Click
  • Message-ID
  • TLS in Received chain (look for ESMTPS)

Required in DKIM-Signature h= field:

  • list-unsubscribe
  • list-unsubscribe-post

Step 10 — Post-flight & ramp-up

  • Reactivate the paused outbound workflows
  • Watch DMARC aggregate reports (rua) for 5–7 days
  • Once 100% pass-rate verified, ramp p=nonep=quarantine
  • Verify domain in Google Postmaster Tools (free, requires DNS verification) — gives reputation score 24-48h after verification
  • Verify domain in Microsoft SNDS if you send to outlook.com/hotmail.com

Common gotchas (learned the hard way)

  1. dmarc@ mailbox didn't exist → DMARC reports vanish silently
  2. Hostinger TXT updates create duplicates in merge mode — must DELETE first
  3. n8n Code node sandboxes crypto module → cannot use HMAC for unsub-tokens, fall back to random + DB lookup
  4. n8n webhook with httpMethod: ["GET","POST"] silently splits into two outputs — use TWO separate webhook nodes feeding the same downstream
  5. n8n executeQuery with queryReplacement splits on commas — values with commas in them break the binding. Build the SQL via Code node with proper escaping instead.
  6. DKIM only signs headers in its h= list. If List-Unsubscribe isn't there, mail clients reject the one-click action.
  7. DMARC at p=quarantine does NOT mean delivery is fine — the receiver decides per-message. Check dmarc=pass in raw headers, not the published policy.
  8. Same DNS, different deliverability → it's reputation/content/volume, not auth. Don't waste time re-fixing already-correct DNS.

Files in this repo

  • SKILL.md — this file (entry point)
  • scripts/ — runnable helpers extracted from the FactBinger production fix
  • examples/ — SQL migration, n8n workflow JSON for unsubscribe + bounce parser
  • docs/CASE-STUDY.md — the full FactBinger case study with timeline + measurements

License

MIT — use, modify, redistribute. No warranty.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.