Install
$ agentstack add skill-pouya-png-mailoutofspam-mailoutofspam ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
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)
- Pause outbound workflows BEFORE making changes. Do not edit a live-sending pipeline.
- Preserve all logs and current configuration — snapshot before mutation.
- No spam-evasion tactics — no domain rotation, no fake
Re:/Fwd:, no hidden unsubscribe. - Do not move DMARC to
p=quarantineorp=rejectuntil SPF/DKIM/DMARC pass consistently. If you find the domain already at quarantine but no aggregate reports verify pass, drop top=nonefirst, verify, ramp back. - 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 usesgoogle, 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. Ifdmarc@doesn't exist, reports vanish — repoint to a verified mailbox likecontact@or create the dmarc@ inbox first. - Ramp to
p=quarantineonly 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:
DELETEthe existing TXT namePUTthe 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 /unsubscribewith form bodyt=&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,}, lowercaseGRATIS|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=passdmarc=pass
Required headers present:
List-Unsubscribe(URL + mailto)List-Unsubscribe-Post: List-Unsubscribe=One-ClickMessage-ID- TLS in
Receivedchain (look forESMTPS)
Required in DKIM-Signature h= field:
list-unsubscribelist-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=none→p=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)
dmarc@mailbox didn't exist → DMARC reports vanish silently- Hostinger TXT updates create duplicates in merge mode — must DELETE first
- n8n Code node sandboxes
cryptomodule → cannot use HMAC for unsub-tokens, fall back to random + DB lookup - n8n webhook with
httpMethod: ["GET","POST"]silently splits into two outputs — use TWO separate webhook nodes feeding the same downstream - n8n
executeQuerywithqueryReplacementsplits on commas — values with commas in them break the binding. Build the SQL via Code node with proper escaping instead. - DKIM only signs headers in its
h=list. IfList-Unsubscribeisn't there, mail clients reject the one-click action. - DMARC at
p=quarantinedoes NOT mean delivery is fine — the receiver decides per-message. Checkdmarc=passin raw headers, not the published policy. - 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 fixexamples/— SQL migration, n8n workflow JSON for unsubscribe + bounce parserdocs/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.
- Author: pouyA-png
- Source: pouyA-png/mailoutofspam
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.